更新至第三代服务器环境 | Docker 萌新体验记

博客算是 lwl 第一个用上 VPS 的项目了,和很多初学者一样,lwl 一直在使用一键包来完成服务器环境的架设。随着时间的推移以及请求量的增加,现有的服务器环境维护变得越来越困难,甚至开始出现了一些无法找到原因(当然还是因为我菜所以找不出来)的问题。为此, lwl 决定开始着手设计和部署第三代服务器运行环境。

问题和需求

前两代服务器环境构建都是由一键包作为核心基础,目前第二代使用的是 Oneinstack。不得不承认 Oneinstack 现在是越来越方便了,lwl 在前几天开始调查的时候惊讶地发现它居然都已经支持无人值守式自动安装了。不过为了一些特殊需求,lwl 需要对 Nginx 的编译参数进行一些更改,因此此前一直只能手动准备环境并安装 Nginx。这样不免会消耗大量时间,尤其是需要部署比较多的服务器时,每一台机器都需要进行手动配置。

值得一提的是,API 服务的请求量在近几个月出现了非常大的上涨,日均请求量已超 70 万次。目前承载 API 的服务器是一台流量计费机器,如此巨大的流量给 lwl 的钱包造成了很大压力_(:з」∠)_,急需将 API 服务迁移至带宽计费服务器或是进行分布式部署以缓解流量压力。同时,当前环境下没有有效的日志管理机制,大量 Nginx 日志记录下后未经分析,无法识别恶意流量以及判别和调度流量。

同样重要的是服务器参数监控。目前 lwl 仅对各服务进行外部可用性监测,但并没有有效机制对服务器各项基本参数进行监控,在出现问题时只能知道 「服务出现了故障」,无法快速定位「什么原因导致了故障」。目前这一机制的欠缺已经导致了一些问题,虽然暂时不显著,但从长远角度看应该预先控制。(这一问题同时也涉及对应用层的日志和错误信息进行处理,但由于本文主要考虑基础环境的改进,故暂时不讨论应用层分析。(其实是 lwl 太菜了暂时还写不出来应用层分析,在等萨摩的分析器(逃))

其次还有自动证书签发问题。目前的自动证书换新机制是由各服务器各自为政进行签发,如果进行分布式部署,这样做势必会导致在一个周期内签发的证书数量大幅上涨,带来潜在的安全问题。同时,分布式部署会导致无法预知对特定请求进行响应的服务器,因此无法使用 web 方式进行签发(无法确定 Let’s Encrypt 的请求会被分配到哪个服务器);而如果使用 DNS 方式签发,则多服务器同时续签时将有可能导致互相覆盖验证记录,同样存在问题。

最后,目前 lwl 的各项服务与服务器环境耦合严重,配置文件分散,各服务器的基础软件包情况不一致,日志分卷处理等功能甚至只在一台服务器上进行部署,导致核心服务可以选择的承载服务器少之又少。这无疑浪费了大量服务器资源,也使维护以及紧急迁移变得困难。

整理一下,基本就是以下几点:

  • 修改编译参数带来的人工参与对部署速度和难度的提升
  • 部分服务体量较大,需要分布式部署
  • 分布式部署带来的证书分发问题
  • 各服务器环境差异较大,影响整体利用率
  • 缺少对服务器环境的监测,没有有效利用日志

既然问题已经明确,那么就可以开始想办法解决了。

解决方案

要进行快速分布式部署,lwl 能想到的只有通过编写 Bash 脚本或者使用 Docker 两种方案。前者的优势在于和目前的环境配置方式比较相似,搭建完成后日常维护学习成本略低一些,而且相较 Docker 资源占用可能略低。不过 lwl 并不熟悉 Bash,而现在又并没有完全符合需求的现成脚本(虽然我也不熟悉 Docker,但是相较来说我对 Docker 更感兴趣一些,并且 Docker 近几年在行业上的应用是有目共睹的),所以最终还是决定使用 Docker 作为第三代服务器环境的基础。为此 lwl 启动了一系列的 LFS (LWL Fundamentals Service) Dockerized 项目,这也是下文将介绍的主要内容。

需要提前说明的是,本文不会完全解答和修复上述的所有问题。因为这些对于 lwl 来说确实不是一个可以几天几周就完成的工程了 ヾ(´・ ・`。)ノ”。lwl 会根据工程进度继续发布相关文章,也欢迎大佬您在评论区提供您的见解和建议,这对我很有帮助~

由于本文撰写进行了很长时间,在 LWL 开始使用 Docker 开发后还发现了另一个非常棒的优势 —— Docker Hub 会自动对镜像进行编译打包,实际上最后部署到服务器上只需要下载一系列很小的镜像就可以立即使用了。如果使用 Bash 脚本,则需要在所有服务器上重新编译一次。之所以前文没有发现这点,是因为当时本来打算将 Dockerfile 全部和 docker-compose.yml 放在一起下发,这样做就会导致需要分别在数台服务器上重新编译。

会出现这样的错误显然是因为 lwl 对 Docker 还不够熟悉就打算照葫芦画瓢的改一改,好在后来查资料的时候看到了一个非常不错的 Dokcer 中文指南,这本指南很详细的介绍了相关概念和实践,而且内容是基于最新的 Docker 18 编写的。在这里强烈安利给有学习 Docker 想法的读者(

LFS 系列组件

本节将向您介绍 LWL 目前已经完成或已有初步构想的 LFS 组件项目 |´・ω・)ノ

Nginx

lwl 一直使用 Nginx 作为 Web Server,本来是有想在第三代换成 caddy 的,不过测试之后发现果然我还是需要一个「功能比较完善,出错易找方案」的软件(太菜了搞不定新问题),所以最后还是真香了。

LFS-Nginx 基于官方 Nginx-Alpine 版本修改,在其基础上添加了最新版本的 Openssl、Botli 压缩等,打上了辰辰大佬的 Patch(Add HTTP2 HPACK Encoding Support && Dynamic TLS Record support && Strict-SNI),并内置了部分通用配置文件。想了解具体编译过程的读者可以参阅辰辰大佬的文章 (但 LFS 版本并没有使用完全一样的编译参数和组件)~

目前该镜像已经发布至官方 Docker Hub,欢迎您在自己的项目中使用~ 同时 lwl 希望让通用配置文件尽可能符合所有人的需求,因此欢迎您发起 Pull Request 或提出 Issue 帮助 lwl 改进这个镜像~

2018 双十一更新:发现 Strict-SNI 的一个坑。首先,在使用 S-SNI 的时候需要至少有两个监听在 443 的 ssl server 段(已知的坑),但是昨天 lwl 部署服务器的时候发现 Patch 文档上给的简易假 Server server { listen 443 ssl; } 不能用了,Nginx 报错提示需要这个假 Server 设置 ssl_certificate。然而 lwl 此前在 API 服务器上使用这个假 Server 并没有遇到问题……一番操作之后 lwl 终于发现,由于 api 站文件夹以 a 开头,而假 Server 的文件夹以 f 开头,因此在 API 服务器上,API 站的配置文件更早被 Nginx 检测到,于是 Nginx 选择 API 作为 Default SNI Server,随后检测到的 Fake Server 就不需要设置 ssl_certificate 了。在新部署的服务器上,所有站的首字母序都比 f 大,因此 Nginx 首选假服务器,报错就发生了。

新的站点配置存放方式

以往 lwl 存放 Nginx 各分站配置的方式是将其整体放在 Nginx 配置文件夹内,并通过在主配置文件中 include 的方式引用。不过在第三代环境中,lwl 希望让站点尽可能「模块化」。新的存放方式将配置文件与站点文件放在一起(见下图),这样就能将站点的配置文件和站点文件一起分发了。

wwwroot
├── blog.lwl12.com
    ├── conf
    │   └── nginx.conf
    └── file

于是这将会非常有利于下一个组件的工作——

Webhook

在第三代环境中,lwl 希望将所有站点文件和配置文件使用 Git 进行管理,同时在版本库更新时,通过 Webhook 机制通知所有服务器自动更新相关文件。
由于部分站点的文件结构比较复杂,目前该组件仍处于评估阶段,需要等待进一步设计非代码文件(如媒体文件)的同步逻辑。

PHP

lwl 在官方 PHP:7-fpm-alpine 镜像的基础上为 PHP 添加 redis 等扩展,从而更加符合 lwl 的使用需求。
然后由于隔壁小海豚大佬的小工具以及本博客邮件队列的需求,很无奈的只能塞了一个 Python 到 PHP 镜像里去……(这样做不符合最佳实践大家不要学啊,都是因为 lwl 太菜了嘤嘤嘤)

Docker Compose / Swarm

上面说了 Nginx 和 PHP 的定制镜像,以及还有不需要定制但是也需要安装的 mysql 和 redis,这些镜像要怎么在实际部署时快速配置环境(humm你应该不会希望使用命令手动指定镜像的参数吧?),并且架设容器间的各类环境呢?Docker 官方给出了答案 —— Docker Compose

嗯于是当然就有了 LFS-Docker-Compose 项目。这个项目主要是为所有镜像设置他们的运行时环境,包括可供他们使用的端口、文件权限(volumes)、环境变量(时区,服务器编号)等参数。有了这个 docker-compose.yml,再加上我一会儿要说的另一个项目,lwl 就可以做到在非常短的时间内配置就绪一台服务器啦~

不过在此之前,让 lwl 先来聊一聊另一个 Docker 官方项目:Docker Swarm,lwl 被这个功能起码坑了总计 3% 以上的服务在线率(逃

这个故事要从 lwl 查文档的时候发现的 Docker 的一个非常有趣的功能 —— secret 说起了。Secret 功能可以用于为容器分配它可以访问的一系列机密信息,例如网站的数据库密码。看到这个功能之后 lwl 非常开心,立刻在 Compose 里改成了用 secret 分配数据库密码信息。结果到了部署测试的时候,MySQL 怎么也启动不了。百思不得其解的 lwl 找来了小海豚帮忙,在两人一番紧张的检查之后……

黑人问号.jpg。查找相关案例后 lwl 发现虽然似乎吹的很好听但是非 Swarm 集群模式下 Docker 是不支持使用 Secret 的,而且虽然 Docker Compose 似乎做了一些 trick 来让非 Swarm 模式也能使用 Secret (文件变成文件夹就是这个原因了……),但是实际用起来非常让人摸不着头脑,还找不到文档。

此时 lwl 内心 OS:好,OK,那就上 Swarm!这东西看起来太好用了可以用 Secret 分发 SSL 证书而且还可以支持服务器间内网还可以批量滚动升级服务器集群上的容器简直是太适合我了哈哈哈哈哈哈哈哈!

结果事实证明我还是太 naive……在兴冲冲的部署 Swarm 之后没多久,lwl 发现了一个尴尬的问题:secret 是不支持动态更新的。这意味着什么呢?如果 lwl 想要更新一个证书密钥,必须在 Swarm Manager 节点上停止所有服务器上的 Nginx 容器,然后删掉原来的 Secret,添加新的同名 Secret,然后再启动服务器。说得更直白一点,服务必须停机才能完成这个更新……

这种问题怎么可能难倒我呢?一通操作之后 lwl 找到了这个 Issue,其中有一位开发者提供的脚本稍作修改就可以适合 lwl 的情况了。于是 lwl 手打了一堆 acme.sh --issue && acme.sh --installcert 并擦了擦额头上的汗,终于解决了……才怪!Swarm 在升级容器的时候会监测可用性,如果有容器挂了就整体回滚。但是因为玄学原因频繁的会有容器在更新证书的时候宕那么一下……于是那个证书就下发失败了。

此时的 lwl:但是好不容易改好的东西啊,虽然有那么点瑕疵但是还是凑合能用的吧,先放着不管好了(

然后果不其然的……

21 日晚,突然在小浩大佬的博客交流群收到 Axton 大佬报告一言 API 异常的 lwl 眉头一皱,发现事情并不简单。想看车祸现场的同学可以点这里

于是正打算去吃饭的 lwl 无奈地在气温骤降的湖南咕了他可爱的晚餐 30 分钟以上用来处理这个问题。最终调查报告显示不知道为什么(API 挂了急着修复没细看)但是用了 Swarm 的容器是没法拿到用户 IP 的……到此 lwl 被气的彻底死心(萨摩:羊驼生气,羊驼炸毛.jpg),于是选择撤掉了搭了一天的 Swarm。

嗯那么既然 Swarm 撤掉了,证书分发就又成问题了。所以很高兴下面这个点子不用被雪藏可以给你们看了↓↓↓

CRDS 证书签发与分发服务

对于前文提到的证书问题, lwl 最初设计了一个中心服务用于签发相关域名的证书并分发给服务器。目前初步设想是使用 acme.sh 维护证书,在服务器上部署客户端(作为 LFS 的一部分),在中心服务器上部署一套 API 实现客户端的注册、分发、更新通知等工作。目前已经大概设计好了首次注册流程,具体见下图:

CRDS 项目仍在设计开发当中(本来差点就死于 Swarm 了),可能会扩展更多功能,具体请期待 lwl 和 meto 大佬的后续文章(咕咕咕)。同时也欢迎在评论区分享您的设想。

Deploy.sh

唔,还记得刚刚在 Compose 那节 lwl 提到的 “再加上一会儿要提到的另一个项目”吗?是啦就是这个 Deploy.sh 辽!

在实际服务部署当中,并不是只把 web 服务拿 docker 装上就可以了的。刚拿到服务器的你一定也和 lwl 一样需要部署一些日志轮转、ssh 密钥/端口之类的信息,而这些是 Docker 无法代劳的。于是就有了 Deploy.sh。这个写的乱七八糟的脚本可以帮助 lwl 快速完成一台服务器的基础设置工作。humm 于是 lwl 就也可以学那些看起来很炫酷(大佬:只有你这个萌新才这么觉得吧?)的操作了:

curl -sL https://deploy.cn2.network/ | sudo bash -s [$lwl-hostname]

(啊注意上面这个代码不要乱跑,否则 lwl 就可以拿着密钥进你家服务器大门啦)

Uptime Page

啊其实这不算一个 LFS 组件(读者:那你说个啥???),不过还是想提一下。

特大利好!特大利好!羊驼又改了个 Uptime Page!(广告打死 可能有读者还记得 lwl 曾经写过一篇建立自己的服务状态页,而由于这个页面是一个改编项目,可维护性相当糟糕,所以最后是处于凑合能用的状态(访问很慢)就一直无人修改了。

不过前几天辰辰大佬给我发了一个他的服务状态页,lwl 一看……好家伙啥时候有这么漂亮的状态页了?谁写的啊?我怎么不知道啊?于是最后知道了是本站的友链站长 GIUEM 写的一个新 Page(哇我这友链站长做的贼不称职,友站发新东西都不知道(虽然他也没往博客上发))。他使用的不是和 lwl 一样的官方状态页数据源,而是直接从 UptimeRobot API 取信息,并且有预加载信息机制,访问速度快了不止一个档次。

于是在 GIUEM 、METO、DIYgod 大佬的指导下,lwl 又丢了一堆 Pull Request 加了一堆功能让这个状态页 (Demo) 用起来更顺手。现在你可以在 Github 找到并部署它啦。

最后

lwl 这篇文章大概写了一周时间,期间是第一次尝试一边部署新的环境一边撰写本文(以前好多新东西因为部署完就忘了怎么做的了于是文章咕了)。所以这篇文章看着也许结构会有点乱,上下文不一定有准确的衔接,还请看官您见谅。同时,本文主要是记录性质而非发布/教程性质,lwl 也是 Docker 初学者,文章描述的方案可能并非最佳方案,因此请您慎重决定是否将这些内容用于您的生产环境。

另外,本文的 Banner 和 BGM 来自番剧《我的英雄学院》(及其衍生作品),非常好看的热血番,给各位安利一下(

那么,到现在为止,您已经读完这篇快五千字的文章啦~ 非常感谢您的耐心,咱们下篇文章见!(发出了咕咕咕的声音)

- EOF -

40 条评论

昵称
  1. 巡视官

    「あたいってば最強ね!」(本小姐最強)

  2. StarryCat

    紧跟julao脚步,最近有时间学习Docker了<img src="” alt=”facial”>

  3. Pingback: AWS Lambda 部署 UptimeRobotPage 状态监测页 - Milkice's IceBox

  4. Justf

    啊嘞我的评论怎么不见了_(:з)∠)_算了重写吧_(:з)∠)_如果使用 LFS-Docker-Compose 的话,是不是挂载点必须是 /data/wwwroot ?可以自定义的吗(Docker萌新瑟瑟发抖x

    1. lwl12

      @Justf 可以改的啊,把涉及到容器的 volumes 映射改一下就行,具体看看文章里提到的 Docker 教程

  5. 土木坛子

    好技术的感觉,几乎从来没有这样折腾过。

    1. lwl12

      @土木坛子 ∠( ᐛ 」∠)_没记错的话坛子好像是跑在虚拟空间上?

  6. Andy

    Docker 真的是太方便了! 追随大佬脚步(๑•̀ㅁ•́ฅ) (真香

  7. Edison Jwa

    我什么时候才能像lwl大佬一样厉害呢 OωO

    1. lwl12

      @Edison Jwa 我没有,我不是,这个才是大佬 -> https://flyhigher.top/develop/1245.html

  8. Leo

    test

  9. Yiveco

    向大佬低头orz

  10. taoxinhao

    用的哪的vps,然后貌似评论表情那里遮住了按钮,iphone7plus,safari

    1. lwl12

      @taoxinhao 评论表情那个异常在移动端上是已知问题……暂时不打算修了,博客现在在腾讯云上

  11. a632079

    啊 是大佬 Orz

  12. jlqwer

    facial

  13. duangsuse

    OωO 好耶 LWL 大佬

  14. kissshot

    serverlessfacial

  15. 援军

    虽然前面看不明白 但是 uptimerobot-page 大赞 (๑•̀ㅂ•́)و✧

  16. Sonic853

    啊 是大佬 我躺平了(:з」∠)

    1. lwl12

      @Sonic853 853 大佬在胡说些什么呢(ó﹏ò。)

  17. FlyingSky

    感受到了大佬的气息∠( ᐛ 」∠)_

  18. 程志辉

    比lwl12更更更更菜的我,不敢说话,评个论就溜😅

    1. lwl12

      @程志辉 你看,我说了会有更新的,没咕((

  19. 惶心

    哦对了最近 又 打起了 Fly 大佬的个人页的主意,在测试的过程中也遇到了文字顺序乱掉的问题,频率一分钟内肯定没有10次。然后今天看不知道是哪里看的 API,他们的限制方案是类似于 30分钟 / 1000 次这样的。感觉 lwl 可以不要用秒来做限制,比方说一个普通用户访问一个引用了 API 的网页,可能也就 5-10 分钟,这样的情况下如果用户多刷新几次 / 多打开几个一样的或者类似的页面,LWL API 就开始出问题了 == 所以可以考虑一个较长的时间周期。

    1. lwl12

      @惶心 限制其实是长周期的,但是对部分特征的请求 quota 会比较小一些(

  20. 惶心

    深深地感到和 lwl 的水平绝对不是同一个等级上的,文章 90% 是看不懂的内容。但是还是觉得 lwl
    很厉害,写文章卖萌也很厉害(((

  21. kn007

    可爱,想X

    1. lwl12

      @kn007 |´・ω・)ノ这次文章主标题致敬更新博主 kn007 (逃

  22. Tony

    哇 深度好文 马克学习

  23. Otstar Lin

    好耶,坐地板了ヾ(≧∇≦*)ゝ

  24. Y2Nk4

    怪不得最近看到lwl的GitHub动态,弄了几个dockerfile项目

  25. Indexyz

    啊其实你可以不撤掉 Swarm 的(跑
    可以把网关的 nginx 换成 https://traefik.io

    Traefik 自带 Let’s 自动签证书 + RealIP, 还能给 service 打 label 之后直接设定出口规则
    facial

    1. lwl12

      @Indexyz (๑•̀ㅁ•́ฅ)感谢铁头大佬,刚开始就多了解了一下 Caddy……
      我再看看 traefik(逃

  26. Mitt

    抢第三facial

  27. Sukka

    呜,地板都没有了

  28. Axton

    好耶是第二(

  29. milkice

    嘤嘤嘤怎么才第二

  30. METO

    好的,我是第一(

    1. Zohar

      @METO 大坏蛋嘤嘤嘤facial