Silence & Solitude makes...

Pu's mind space

十年

—无韵集句 和小智

青山遮不住
毕竟东流去
十年扬州梦
天凉好个秋

总角宴晏晏
岁月忽已晚
幽并游侠儿
萧萧斑马鸣

<转/译>如何将应用配置打包进Docker

粗译

新手开始部署他们的第一个Docker Container的时候经常会问的一个问题就是:”我该怎么把应用的配置打包进Docker容器里面?”.问题中提到的配置包括worker的数量,JVM的内存大小,或者数据库的连接串.现实中实现这些有一套标准方法,而它们各有利弊.

将配置发送到Docker打包了的应用里的三个半方法

1. 将配置烧进容器里

这可能是最简单的配置了,也就是在Dockerfile里面利用COPY指令把配置文件扔到容器里,或者通过RUN指令执行sed以及echo等命令来修改默认的配置文件.

优点:

缺点:

  • 因为配置已经烧到镜像里,未来可能的配置改动都需要修改Dockerfile并重新build来实现

2a. 利用环境变量动态地配置应用

这是一个在Docker Hub上的镜像比较常用的方法,譬如PostgreSQL的’POSTGRES_USER’和’POSTGRES_PASSWORD’这两个环境变量.

简单地说,当你启动容器docker run的时候,需要指定环境变量,而容器的进入点(Entry point)也就是启动脚本会寻找这些变量,并把他们用在相应的配置文件里.

需要提的是,启动脚本应该提供有意义的缺省值,以防用户没有提供这些环境变量.

优点:

  • 从配置角度看,这样的容器更灵活

缺点:

  • 打破了生产/开发环境的一致性要求,也就是说其他人可以通过不同的配置使得容器的行为在生产/开发环境下不一致
  • 一些复杂的配置无法映射为简单的键值对

2b. 利用环境变量动态地配置应用

和上面的一样,但是启动脚本是从网络,像是Consul或者etcd来获取配置参数.

这就使得通过环境变量来实现复杂配置成为可能.因为键值对存储器可以有多层结构.值得一提的是有很多工具可以用来抓取键值对并替换到你的配置文件中.而像confd这类工具甚至可以在配置改变时自动重新加载应用.这就使得应用的配置更加动态化.

优点:

  • 配置更动态
  • 键值存储使得复杂配置成为可能

缺点:

  • 同样违反了生产/开发环境一致性(译注:和所谓的immutable container是同一个概念)
  • KV store必须高可用,因为所有的容器配置都依赖于这个存储器

3. 将配置文件目录挂载到Docker Volumes

Docker Volume介绍略

如果配置文件正好在运行容器的文件系统上,可以通过挂载的方式提供到容器内部使用.例如

docker run -v /home/dan/my_statsd_config.conf:/etc/statsd.conf hopsoft/graphite-statsd

优点:

  • 无需修改容器就可以随意配置

缺点:

  • 违反生产/开发环境一致性要求
  • 如果要用在生产环境中,必须要保证配置文件在基础操作系统中可用.

结论

如上所述,想要配置一个容器包里的应用,方法其实很多,各有利弊.哪个最好?其实取决于你需要多大的动态配置的自由度以及你是否愿意承担额外的譬如维护一个KV store这样的负担.

译后评

个人目前倾向1和2a,但是还没有在生产环境中体验过实际会有哪些坑,所以只能是倾向.

原文地址

一次搭建高性能Nodejs httpServer的尝试(2)

继续上次的.
上次搞的node http server,一个简单的服务器,接收HTTP POST,解析数据入mongo库(nodejs driver用的native driver而非mongoose),主要做了下面的一些优化:

  • 系统层面放开同时打开的文件数(也就是socket数)的限制
  • 复用链接(主要是ab http1.0 keepalive的坑)
  • 增加并发
  • “优化”业务逻辑

其实都是一些”预”优化,在没有需要针对处理的问题出现的情况下先放开一些东西. 后来测试结果满意,一个小项目开始,两周完成了一个小业务之后,需要部署让测试介入.环境一换,问题就来了.

原先在本地的Linux台式机上近16000的TPS到了openstack(4core, 4GB memory)的VM上TPS瞬间掉到了小几千.赶紧登陆上去vmstat查看,发现Procs的r值(等待执行的进程数)接近20,远大于CPU核心数,也就是说CPU成了应用的瓶颈.–原来台式机是8X3.4GHz,到了虚拟机上只有4X2.3GHz,早该想到!

提高虚拟核心数!当然同时也把node.js的slave process提高到8个.同时,把libuv的线程池的限制也放开(设置UV_THREADPOOL_SIZE=8的环境变量),至于这个有没有用,其实我也是不太清楚的,顺便贴一个关于这个问题的争论—好吧,其实是撕逼,吃瓜群众表示看神仙打架也很有意思嘛—也不知道libuv团队最后有没有采纳这个不礼貌的家伙的建议.但是不管libuv的线程池是否应该跟核心数相等,个人觉得不管怎么说,mongodb的native driver开的线程池的大小是5,如果libuv的线程池大小只有4的话肯定不能发挥出其期待的性能吧.再次顺便提一句,mongodb nodejs官方的driver其默认线程池大小也是可以改的–详见参考文档,但是我这里并没有尝试.

好吧,再次测试,TPS又恢复到了16000+. Done!

之后又简单看了一下GC/内存有没有潜在的问题,这也是上一篇文末希望做的.简单来讲做法就是使用node-inspector+Chrome DevTools来比对heap snapshot(和参考文章的工具小有差异)以及使用–trace_gc来查看请求量大量/持久的压力下GC的时间是否能够接受.
但是由于压力较小,没能发现什么—于是就草草地交给测试了.

希望这个DEMO项目能做下去,这样之后说不定在使用中真的就能碰到大数据量的情况,来真正被需求倒逼去进一步探索其他的性能瓶颈—作为一个敏捷开发者(傲娇),我们不提倡提前优化,哈哈.

开发环境的虚拟化

随着Docker越来越火,产品的持续集成,测试,甚至交付(部署)都越来越倾向于使用容器来进行’标准化’.痛苦往往发生在不明白为什么要这么做就被迫跟着走.
但是用着用着确实能感觉到一些方便的地方.我想作为产品开发者,应该是这个Docker化的方案里受益最少的了吧.

最近在关于开发环境如何和Docker集成颇感头疼.最好的结果是开发者能在开发机器上直接使用于测试团队一致的环境进行初步测试/单元测试.对于脚本语言还好–因为开发者需要的其实仅仅是一个文本编辑器而已.但编译语言就不同了,需要编译.如果需要把测试人员的那一套弄到开发者本地,编译这一步到底应该在Docker里面做还是在Docker外面做?拿java来说,IDE一般都支持热部署,其效果就是修改某个java文件后不需要整个替换war包,IDE应该是增量替换了explored的artifact里面的部分class,重新build是很稀少的情况,因此代码-ide-测试是联系在一起的.而如果要测试基于Docker的产品,那么必然需要对每一个改动进行build,对于大的项目,这显然是个灾难.

2017-08-20 更新

去年这个时候的理解还是比较肤浅的,更新一下:

首先澄清初始问题,依据12-factors的要求:Make the tools gap small: keep development and production as similar as possible.
现在看来这里要求的开发和生产环境的一致性其实是在要求CI/CD环境的一致性,或者具体点,是Jenkins Build Node和Production Node的一致性。由于容器技术的引入,其实小环境一致性的要求是自然满足的,而Node系统以及Docker-Engine本身(版本的)一致性,可以很容易地通过复用诸如Chef recipes或者Openstack Heat Template来达到,至于容器编排器的不一致性 – 譬如在Jenkins Slave上用的是docker-compose而在生产环境中使用Kubernetes – 可能造成的问题,我想是Operation需要解决的,毕竟DevOps的存在只是为了让Ops日子好过点,而不是让Ops失去存在的必要。

然后回到开发者视角,具体的问题是:开发者本机上跑的那个开发中的应用/服务应该以容器的形式调试吗?
这是本文最初关注的问题,因为我当时把开发环境理解成了开发者环境,现在看来这个问题已经没有多少意义了,答案是随便。如果是java开发的话,可能你本机的jdk版本跟容器里的jre版本一致比较好,但是其实也无所谓,如果有一些这种环境差异带来的问题,也会以CI的单元测试失败的形式暴露出来。甚至你的开发机器也可以不需要装Docker,因为有Contract Test的保证,你的应用/服务与其他组件集成时可能出现的问题也会在CI过程中以inter-service测试失败的形式暴露出来。当然如果你希望少一点这种类型的返工,还是需要尽量保持开发机器中的单元测试和CI环境下的环境经可能一致 – 在CI里尽可能调用Gradle Task可以很好地帮助我们实现这一点 – 但是这不是必要的,我想说的是12条军规中的关于开发和生产的环境一致性要求是在要求CI和CD的一致性,而非开发者环境和CI环境的一致性,所以作为开发者来说,这个问题可以忽略。

重听旧日的足迹

我已经不知道初听这个曲子的时候是什么情景了,只知道是在15,6岁的时候,我还在家乡的小镇子上上初中。当时不清楚那将是我在故乡生活的最后两年,之后一路走远,高中到城市里,大学出了省,如今又在另一个城市生活。家乡是回不去了。

刚从生理和心理上开始往一个独立的人发展,这首歌是那个阶段的背景音。我不知道是因为它本身自带的怀旧情愫还是因为我自己初听此歌时的情境在我的脑海里给这段音乐打上了时空烙印,再听到这个曲子的时候颇有感触,画面感很强烈。我想把它画出来,可惜没有绘画的本领,只好试图用语言来刻画–虽然语言也不怎么样。

先是一段钢琴solo,很轻很静,像是怕打断已经如此脆弱的记忆,怕破坏已如此模糊的印象–就像我已经不记得初中不再联系的那些同学的相貌,2000年的冬天上学路上会听到什么–架子鼓和贝司都不敢介入,任由琴音缓缓流淌,试看能否触到往日的情景。

琴键点点滴滴,记忆寻寻觅觅,我的25岁在干什么,我的20岁在干什么,我15岁的时候又在干什么?15岁,啊,我曾经无邪的15岁呀,我想起来了。

–Band就都进来了,吉他和架子鼓,那么热烈。就像我的15岁也曾经那么热烈,就像我的家乡也曾经车水马龙,也曾经有如吵架般拉家常的老人,有忙碌向上的中年人,有花花绿绿的孩子们。有金融,轻工业,手工业和农业,我15岁的时候就在他们中间,没有意识到这些马上就要消失,现在再想回去找却也找不见了–家乡变得太快,而我也失了凝视的眼。

–像是怕赶不上自己的回忆,家驹的吐词节奏很轻快,追着记忆跑,忘记了抒情。

他梦萦家乡,只记得那一片绿油油,只记得她,想到就要回乡了,他并没有克制自己的关于她的回忆。

而回到实际中的家乡时,他才发现旧人多已不在,虽然还有着一样的安谧,一样友好的乡音,但却都是陌生的了。然而家乡的夜晚还是那样熟悉,就像那时候的夜晚,作为独生子女,在家乡,大抵住着比如今安身的租住房宽敞得多的房间,一个个独处的夜晚,从来没有物质上的幻想,宇宙,爱情甚至是一些异怪故事都是睡前餐。而和她在一起共度未来的憧憬,大概是幻想得最多的事情吧--虽然对于未来其实没有一点概念。

第二天早起的时候,妈妈已经做好了早饭,还是像从前那样清淡。吃饭的时候,妈妈看似不经意地提起了她,说起她在县城的工作很不错,嫁的人家也挺富裕……他不知道妈妈为什么要给他讲这个故事,也不打算告诉妈妈她曾可能跟这个人成为一家人。

还好有一个老友还住在家乡,他买了水果去带给他的孩子。去往他家的路十多年没有修了,车虽然也能开进去,他还是喜欢走路,虽然远了点,但是慢点也好。

老友托他介绍城里的工作,他应了下来,心里却不希望老友离开妻儿出门打工。

回头的时候,太阳已经快下去了,晚霞如幻。

路过了她的老家,隔河忘去屋子里不像是有人住了,曾经每次路过这里,他都会扭头望向屋边小桥,希望她也恰好去上学。

迎着晚霞又走了几步,路过了两个打闹的孩子,衣着鲜亮,笑声悦耳。

每一张可爱在远处的笑面
每一分亲切在这个温暖家乡故地
已过去的不可再今天只可忆起
一双只懂哭的眼落泪又再落泪
呜……

一次搭建高性能Nodejs httpServer的尝试

老板让尝试用node搭建一个高TPS的http服务器,业务不重要,仅仅测试一下传说中的适合I/O的技术能比java web container好多少.英文版测试结果:

Tried several approach to increase the TPS of a node.js http server to check if it’s competitive to be a easy tool for some specific tasks.
I create a simple http server, based on nodejs native http server. It receives http requests, records its information into a (remote) mongo DB, then response with ‘Okey’.

Test tool is Apache Bench, installed in the same host machine with http server: a desktop of “Dell OptiPlex 7010” with 8 core CPU as well as 8G memory running Oracle Linux Server 6.8.

Optimization approaches include:

  • Increasing the host server’s ‘open files limit’ with “ulimit -n 99999” while the default is 1024, also increasing the default stack size for V8 in node with '--max-old-space-size 2048'

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [root@pu ~]# ulimit -a
    core file size (blocks, -c) 0
    data seg size (kbytes, -d) unlimited
    scheduling priority (-e) 0
    file size (blocks, -f) unlimited
    pending signals (-i) 31197
    max locked memory (kbytes, -l) 64
    max memory size (kbytes, -m) unlimited
    open files (-n) 99999
    pipe size (512 bytes, -p) 8
    POSIX message queues (bytes, -q) 819200
    real-time priority (-r) 0
    stack size (kbytes, -s) 10240
    cpu time (seconds, -t) unlimited
    max user processes (-u) 31197
    virtual memory (kbytes, -v) unlimited
    file locks (-x) unlimited
  • Re-use TCP connections for successive requests, i.e. make use of the Keep-Alive feature of http(1.0)

  • Make the http server a cluster, make use of more cores of CPU.
  • Change the business logic, return immediately when receiving request instead of waiting for database finish recording.

OS level tuning

Increasing the max open files(and hence the sockets) as well as stack size didn’t improve the performance. Which means we haven’t reached the limit of parallel socket numbers, nor memory limit.

Reuse connection

The http header ‘Connection: keep-alive’ is needed for Http/1.0 to reuse connection for further request— while for Http/1.1, the connection is keep-alive by default. Apache Bench is using 1.0, and with a parameter “-k”, it will add the “keep-alive” header.
As Http/1.0 can’t make use of ‘Transfer-encoding: chunked’, there’s only one possible way for the client to determine the boundary of successive requests in a single connection, i.e. ‘Content-Length’, it’s easy to know the content-length when requesting static file, but for the case of dynamic page, we need to manually calculate the ‘Content-Length’ and then mark it in the response header. And this is what we do by adding code in the node.js http server.
By doing this, the throughput increased:

1
2
3
4
5
6
[root@pu ~]# ab -n 10000 -c 10 http://localhost:1337/
Concurrency Level: 10
Time taken for tests: 1.512 seconds
[root@pu ~]# ab -n 10000 -c 10 -k http://localhost:1337/
Concurrency Level: 10
Time taken for tests: 1.144 seconds

Introduce concurrency

It contains two aspects when we introduce concurrency:
Adding the concurrency level of the test client
Adding the concurrency level of the http server
Since we are doing test in the exact server where http server deploys, the bottleneck can shift between client and server. So adding the concurrency level blindly won’t always increase the performance.
Adding the concurrency of Apache Bench is easy, just increase the parameter value of “-c”, adding this value will increase the TPS, but only valid in a certain range, approximately 1-50, in this range increase concurrency level will increase TPS, but out of this range, the TPS didn’t increase, –and also won’t decrease. For example if you increase the concurrency level to a non-sense high value, it won’t increase the TPS as 50.
To add the concurrency of Nodejs Http Server, we use node’s build-in feature of ‘cluster’, creating several slaves to strive for a single port. After several tuning, I find the concurrent level of 4 slaves increases the performance better, unlike the Apache Bench, adding the concurrency level of Http Server bigger than 4 will cause the total TPS decreased– this is because it will occupy the CPU resources that used for Apache Bench.

1
2
3
[root@pu ~]# ab -n 10000 -c 100 -k http://localhost:1337/
Concurrency Level: 100
Time taken for tests: 0.794 seconds

It is argued that several slaves striving for the same port is not so efficient than four slaves listening to different port respectively, and in the front, adding a inverse-proxy like Nginx to balance the load. This approach is not tried yet.

Change business logic

I tried to remove the code snippet of writing to mongo db, and then test it. In this situation, node.js server has the same TPS as Apache Httpd server.

1
2
3
[root@pu ~]# ab -n 10000 -c 100 -k http://localhost:1337/
Concurrency Level: 100
Time taken for tests: 0.251 seconds

So for static page, nodejs is not so powerful, it’s value lies in when the business logic added, the TPS won’t drop down rapidly.

Future test

Stability: as last time I tried this http server, it shows periodically TPS down, probably related with V8’s GC, so need to investigate into more detail about it.

Update

重开博客记

又开始写博客了,感觉不错.
博客中断的直接原因是VPS和域名服务到期而我想更换,所以没有直接续费仅仅作了简单的备份.间接的原因则是换工作之后生活节奏和状态的改变.我发现换了一个闲的工作之后反而没有了加班加点之后继续利用剩余时间学习的劲头了,奇怪.
间断的一年中除了换工作,还有两件事情.
一个是戒烟复吸了.看起来之前的战略上轻视对手是正确的–我一开始就知道我可以轻易地戒掉,但是战术上显然不够重视,以至于几个月之后重又复吸起来.
一个是碗碗死掉了…
好吧,经历了这么些事情现在又到了一个平静地状态,让人觉得下一个时间轴上的坐标性事件可能要在很久之后了—日子在加速,我在静静的颓败.重开博客大概也是因为有这种恐惧的驱使吧.
不管怎么样,现在又启程了,日子快也好慢也好,我认真地记录.

和一只兔子

周天的早晨,去阳台放碗碗进屋,然后回头去刷牙煮咖啡。它就这么跟着我转啊转,我到哪里它就跟到哪里。我停下来,摸摸它,然后继续干我的事情,它还是这么跟着我,我感到一阵愧疚,不知道该如何满足它,我不能承受它抛弃整个自我而全身心的就这么蹦跳着跟着我。

我突然想到,所谓感情,譬如除却了言语和物质的男女之恋,也不过如此而已。你不可能了解她,她也不会了解你,你喜欢它阳光下的绒毛,它喜欢你的温暖(或者食物)你们仅仅为了互相的陪伴和跟随就可以付出自己全部的感情。

序曲,前戏和其他

电影《Leon》里的变态警察如是说:

screenCapture

leon

序曲让人热血沸腾,它是如此铿锵有力,但序曲过后,说实在的,真他妈的有点闷。

我深以为然,听一支摇滚,在安静中你期待第一个音符的出现,想要知道叩开你心扉的是一个琴键的敲击,一个有力的鼓点异或是吉他的一下拨动。好吧,是吉他,一段清澈的solo,哦也,架子鼓也加进来了,电子音,旋律快起来了……一切都很带感,直到一个意料外的人声合着一团乱麻的乐器音开始唱歌了。怎么说来着,boring,你可以把播放器最小化开始刷微博或者干其他什么了。

在这个周末的早晨,睡到10点自然醒,起床洗漱,打开阳台的窗,阳光照进来。我打开音乐,开始煮咖啡,拖地,把洗衣机里装满衣服并让它转起来,淘米熟饭,把冰箱里的肉拿出来放进微波炉化冻,把蔬菜切块……我就这样饿着肚子,忙活了一个多小时,在洗菜切菜时陆续听到滴滴咚咚的声音:微波炉说化冻成功,电饭煲说饭已OK,洗衣机喊我转完啦。那种感觉真的很美妙,一切都生机勃勃,所有的事情都有条不紊地走向成熟,呵呵,好一个周六清晨的居家交响乐。闻着烹香的米香肉香咖啡香,坐在干净的餐桌旁,突然觉得这一切有点奢侈,我想要留住它,可是肚子实在饿得受不了了,狼吞虎咽起来。8分钟后,我一早上梦幻般美丽的2小时结束了,留在我面前的是油腻的餐盘,难听的音乐,还有需要晾的衣服。说实话,饱着肚子看油腻的饭菜就和一次错误的高潮之后看留下的精液一样恶心。当我眼中烹香美味的肉末茄子变得恶心时,我知道,序曲过去了。Oh,no,亚美爹,嘛跌。

其实又何止是一个早晨呢,一段音乐,一根烟的事件里,或者一次性事,或者一场爱恋里,都有这种序曲美妙过程无聊(甚至事后懊悔)的起伏啊。
你会记得第一次约会时风的沁凉,却记不清第23次做爱的地方。你喜欢点起一支烟的从容,却不喜欢匆匆掐灭的慌张。你们慢聊,我先刷碗去了——哈哈,这样在序曲戛然而止才是正道吧!