Language Matters

1. 语言对新手很重要

每当听到其他人说 “***是世界上最好的语言”,都不免换来一阵嘲讽和讥笑。毕竟在程序员的世界,菜鸟才谈语言,高手都聊架构和方法论。

回想笔者几年前还在学校苦熬时间的时候,常常为了毕业找工作愁上心头。二流学校的二流专业,老师会的东西都是陈芝麻烂谷子的技术。导师非要我们用 C# 来开发那些个 toy project,而我更早接触的是 Java,彼时阿里巴巴已经通过 Java 技术栈跻身三巨头之一。

我该学什么语言呢?我该学什么框架呢?抱着这个疑问,我把目光投向了我的师兄们。

大师兄务实地告诉我:C# 的用途大多在 Windows 的图形化桌面开发。C# 有优势的特定领域是地理信息系统(GIS),但是他估计彼时 C# 和 Java 的岗位的招聘比例大概是 1:5。

另一位师兄开始告诉我那句著名玄而又玄的话:“语言到最后是相通的,只要一门语言学精通了都一样”。当时我就有些疑惑,按照师兄的说法各大公司招聘为什么还限定语言呢?

本着校训“求真务实”,我还是参考了大师兄的意见,选了 Java 作为我的主语言。

一晃五年过去了,笔者也从小菜鸟成长为独当一面(自封的)的工程师了,心里只有一句“老子信了你的邪”想送给第二位师兄。

回想以前上编译原理的老师曾经说过:“等你们工作久了以后,你们的价值不在于你们的技术,而在于你们在这个行业的知识的积累”。但是结合我的工作经历,我还想加一句,作为技术工作者你从事的领域往往是和你所拥有的技术是相关的。

对于新手来说,你选择的主语言在相当长时间内会决定你从事的技术领域。

现在互联网行业所使用的计算机技术是如此复杂,核心技术都不是单个小团队能承载和维护的了。“背靠社区好乘凉”已经成为了工程师的共识。在这种背景下,跨个人、跨团队、跨公司甚至跨国籍的团队协作就成了趋势。

你和外国人交流第一个考虑的是什么?

语言!

2. 收敛技术栈是一种好的工程实践

和人沟通都要看语言,更何况写代码呢?

对于开发者来说,每一种语言都意味着巨大的学习和维护成本,降低项目的开发和运维成本是每一个工程师基本需求。

对于公司来说,收敛技术栈可以降低各种基础服务的成本。举个例子,代码的测试、上线、部署、监控、第三方组件的管理等服务,运维成本与语言数量成正相关。

对于 HR 来说,收敛的技术栈可以降低招人的难度。要找一个精通单一语言的人远比找一个熟悉多门语言的人简单的多。

3. 语言有各自擅长的领域

当前开源世界显现出一个新的趋势:特定的语言更容易在特定的领域发挥出光彩。

比如当红炸子鸡 Go 语言,凭借协程的特性在高并发场景下独占鳌头。我等 Javaer 还在为多线程苦恼的时候,Goer 已经在业务领域攻城略地。

但是反观大数据领域:Hive、Spark、Presto、Flink 等一众计算引擎都是构建在 Java 或者 Scala 这些基于 JVM 的语言,Go 在大数据领域似乎没有什么存在感。

php 在部分领域也许的确是“世界上最好的语言”,但是你要是告诉我用 php 写一个操作系统,不用细想我也知道你不靠谱。

目前国内的公司似乎都没有培养员工的耐性,开箱即用是基本要求,新手在切换技术领域的时候很难跨过这第一步。在用人单位看来:使用的语言都不一样,那更不会有相关领域的积累了。

4. 选择领域而不是语言

在我看来与其选择语言,不如选择技术领域和方向。

作为一个 Java 的使用者,作为一个大数据方向的从业者,我对一直对 Java 抱有信心。

首先,这是中国最主流的语言,背靠阿里巴巴和强大的社区生态。往上浮,可以去做业务 CRUD;往下沉,可以去研究各种中间件和大数据引擎的源代码,Java 是大数据领域的语言霸主毋庸置疑。语言总在相互学习、相互借鉴中发展,比如 Java 8 引入 Lambda 表达式和 Stream API 之后基本就重塑了整个 Java 语言。

悲伤的是,工程师往往需要工作几年之后才有足够的知识和经验去判断哪一个方向值得进行挖掘和探索,这又与上文探讨的问题的初衷又形成了一个悖论。

不过好在现在的年轻人接触前言技术越来越早,我读到研究生一年级还在为语言摇摆不定,对比现在的孩子在大二已经开始基础大数据和机器学习了。真是长江后浪推前浪,前浪死在沙滩上。

2020 的新年感想

往年的新年都是在饭店和亲戚家吃吃喝喝中度过的,今年难得一次全家人都只能在家里待着。

今年过年的主要工作都在和爸爸一起做饭、洗碗。

之前看纪录片听到一句话说:“中国家庭制作食物都有一家人一起协作的现象。这不仅是一种增进家人情感的传统,更是家族文化的传承!”。

深以为然。

jq 使用简介

1. jq 介绍

工作中经常要做一些额外的 json 格式的数据处理。一般是从接口获取了数据,要做后处理从中提取某些字段。但是单独写一个 Java 程序来处理还要定义一系列 POJO,如果使用 python 来写呢虽然好一些,但也是一个接口一个脚本。
后来发现了 jq 这个工具,一开始只是用它来做 json 格式的美化,后来发现处理 json 数据还真是一把好手!
Mac 下通过 brew install jq 就能安装。下面简单枚举了一下一些简单的使用场景,主要是照着 jq 常用操作 做了一下练习。jqplay.org 这个站点也给了在线编辑器和示例供大家学习。

2. 使用操作

2.1 对 Object 操作

2.1.1 获取某个 key 的值

格式:.<key>.<key>? 表示字段可能不存在)、.<key1>.<key2>.["<key>"]

 # input:
 {"foo": 42, "bar": "less interesting data"}
 
 # command:
 .foo
 
 # output:
 42

2.1.2 获取所有 key 的名称

格式:keys

 # input:
 {"abc": 1, "abcd": 2, "Foo": 3}
 
 # command:
 keys
 
 # output:
 [
   "Foo",
   "abc",
   "abcd"
 ]

2.1.3 获取所有的值

格式:.[]

 # input:
 {"url": "mozillazg.com", "name": "mozillazg"}
 
 # command:
 .[]
 
 # output:
 "mozillazg.com"
 "mozillazg"

2.1.4 所有 value 组成的数组

格式:[.[]]

 # input:
 {"url": "mozillazg.com", "name": "mozillazg"}
 
 # command:
 [.[]]
 
 # output:
 [
   "mozillazg.com",
   "mozillazg"
 ]

2.2 数组操作

2.2.1 取出所有元素

格式:.[]

 # input:
 [{"name": "tom"}, {"name": "mozillazg"}]
 
 # command:
 .[]
 
 # output:
 {
   "name": "tom"
 }
 {
   "name": "mozillazg"
 }

2.2.2 切分

格式:.[<start>[:<end>]]

 # input:
 [{"name": "tom"}, {"name": "mozillazg"}, {"name": "jim"}]
 
 # command:
 .[0:2]
 
 # output:
 [
   {
     "name": "tom"
   },
   {
     "name": "mozillazg"
   }
 ]

2.2.3 操作 object 数组中的字段

格式:.[].<key>.[]|.<key>(管道) 或者 map(.name)

 # input:
 [{"name": "foo"},{"name": "bar"},{"name": "foobar"}]
 
 # command:
 .[].name
 
 # output:
 "foo"
 "bar"
 "foobar"

2.2.4 使用多个 filter

分隔符:,

 # input:
 {"url": "mozillazg.com", "name": "mozillazg"}
 
 # command:
 '.url, .name'
 
 # output:
 "mozillazg.com"
 "mozillazg"

2.3 高级操作

2.3.1 管道

符号:|

 # input:
 {"url": "mozillazg.com", "name": "mozillazg"}
 
 # command:
 .|.url
 
 # output:
 "mozillazg.com"

2.3.2 length 函数

2.3.2.1 对字符串
 # input:
 {"url": "mozillazg.com", "name": "mozillazg"}
 
 # command:
 .url|length
 
 # output:
 13
2.3.2.2 对数组
 # input:
 ["mozillazg.com", "mozillazg"]
 
 # command:
 .|length
 
 # output:
 2

2.3.3 map 函数

 # input:
 ["mozillazg.com", "mozillazg"]
 
 # command:
 map(length)
 
 # output:
 [
   13,
   9
 ]

2.3.4 filter 函数

 # input:
 ["mozillazg.com", "mozillazg"]
 
 # command:
 map(select(.|length > 9))
 
 # output:
 [
   "mozillazg.com"
 ]

2.3.5 join 函数

 # input:
 ["mozillazg.com", "mozillazg"]
 
 # command:
 .|join(" | ")
 
 # output:
 "mozillazg.com | mozillazg"

2.3.6 字符串拼接

 # input:
 {"url": "mozillazg.com", "name": "mozillazg"}
 
 # command:
 "hi " + .name
 
 # output:
 "hi mozillazg"

2.3.7 split 函数

 # input:
 "mozillazg.com | mozillazg"
 
 # command:
 split(" | ")
 
 # output:
 [
   "mozillazg.com",
   "mozillazg"
 ]

2.3.8 判断语句

语法:if .. then .. elif .. then .. else .. end

 # input:
 [0, 1, 2, 3]
 
 # command:
 map(if . == 0 then "zero" elif . == 1 then "one" elif . == 2 then "two" else "many" end)
 
 # output:
 [
   "zero",
   "one",
   "two",
   "many"
 ]

2.3.9 构造数组或对象

语法:[] 以及 {}

2.3.9.1 构造对象
 # input:
 ["mozillazg.com", "mozillazg"]
 
 # command:
 {name: .[1]}
 
 # output:
 {
   "name": "mozillazg"
 }
2.3.9.2 构造数组
 # input:
 {"url": "mozillazg.com", "name": "mozillazg"}
 
 # command:
 [.name, .url]
 
 # output:
 [
   "mozillazg",
   "mozillazg.com"
 ]
2.3.9.3 通过数组构造复数个对象
 # input:
 {"name": "mozillazg", "ages": [1, 2]}
 
 # command:
 {name, age: .ages[]}
 
 # output:
 {
   "name": "mozillazg",
   "age": 1
 }
 {
   "name": "mozillazg",
   "age": 2
 }

3. 参考资料

iptables 使用简介

1. 常用语法

 iptables [-t <table>] [-I|-D|-A|-C|-R|-L] <chain> -p <protocol> --dport <port> -j DROP
 
 # 追加、检查、删除规则
 iptables [-t table] {-A|-C|-D} <chain> <rule-specification>
 # 插入规则
 iptables [-t table] -I <chain> [<rulenum>] <rule-specification>
 # 替换规则
 iptables [-t table] -R <chain> <rulenum> <rule-specification>
 # 删除规则
 iptables [-t table] -D <chain> <rulenum>
 # 持久化规则
 iptables [-t table] -S [<chain> [<rulenum>]]
 # 列举规则
 iptables [-t table] -L [<chain> [<rulenum>]] [options...]
 
 rule-specification = [matches...] [target]
 match = -m <matchname> [per-match-options]
 target = -j <targetname> [per-target-options]

其中:

  • -t <table>table 默认值为 filter,包含三种内建的 chainINPUTFORWARDOUTPUT

  • -A|-C|-D|-I|-R|-S|-L-A 追加规则,-C 检查规则,-D 表示删除规则,-I 表示插入规则,-R 替换规则,-S 持久化规则, -L 列举规则。

关于 rule-specification 的部分:

  • -p <protocol>:表示协议,比如 tcpudp

  • -s <address>[/<mask>][,...]:指定来源。

  • --dport <port>:目标端口。

  • -j <targetname>:包含ACCEPTDROPRETURN

2. 使用的简单示例

通常在设计分布式的模块时,都要考虑网络或者服务器故障时的降级与异常处理。因此使用 iptables 模拟网络故障就非常必要,下面给出一个简单的例子。

 # 首先启动一个 HTTP Server
 $ python -m SimpleHTTPServer
 Serving HTTP on 0.0.0.0 port 8000 ...
 
 # 列举所有 chain 
 $ sudo iptables -L
 Chain INPUT (policy ACCEPT)
 target     prot opt source               destination
 
 Chain FORWARD (policy ACCEPT)
 target     prot opt source               destination
 
 Chain OUTPUT (policy ACCEPT)
 target     prot opt source               destination
 
 # curl 一下
 $ curl localhost:8000
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
 <title>Directory listing for /</title>
 <body>
 <h2>Directory listing for /</h2>
 <hr>
 <ul>
 <li><a href=".bash_logout">.bash_logout</a>
 <li><a href=".bashrc">.bashrc</a>
 <li><a href=".lesshst">.lesshst</a>
 <li><a href=".profile">.profile</a>
 <li><a href=".ssh">.ssh</a>
 </ul>
 <hr>
 </body>
 </html>
 
 # 禁止 8000 端口入流量
 $ sudo iptables -I INPUT -p tcp --dport 8000 -j DROP
 
 # 再列举一下所有 chain
 $ sudo iptables -L
 Chain INPUT (policy ACCEPT)
 target     prot opt source               destination
 DROP       tcp  --  anywhere             anywhere             tcp dpt:8000
 
 Chain FORWARD (policy ACCEPT)
 target     prot opt source               destination
 
 Chain OUTPUT (policy ACCEPT)
 target     prot opt source               destination
 
 # 再次 curl 一下
 $ curl localhost:8000
 curl: (7) Failed to connect to localhost port 8000: Connection timed out
 
 # 删除禁用规则
 $ sudo iptables -D INPUT -p tcp --dport 2554 -j DROP

3. 参考资料

沙茶海鲜炖萝卜

  • 配料表:

    1. 白萝卜 1 kg

    2. 金钩海米 100g

    3. 冷冻虾夷扇贝肉 400g

    4. 皇牌沙茶王 100g

    5. 生抽 4 勺

    6. 大蒜数瓣

  • 注意点:

    1. 白萝卜去皮切成滚刀块。

    2. 冷冻扇贝肉用热水解冻,金钩海米用热水浸泡。

    3. 热锅冷油下大蒜、海米炒香,然后加入扇贝肉和沙茶酱翻炒均匀。

    4. 加入白萝卜继续翻炒,给萝卜表面上色。

    5. 将炒好的料加入铸铁锅,把刚才解冻扇贝肉和泡发海米的水加入铸铁锅没过萝卜,大火烧开。

    6. 加入生抽,小火炖煮一个小时。

  • 经验总结:

    1. 无需额外调味,沙茶酱自带咸甜风味。

    2. 炖煮多余的汤汁可以加入虾子面做成一碗沙茶面。

 

WechatIMG139

WechatIMG138

记一个 Fabric8 K8S Client 的坑

1. 前言

最近在作业调度系统上开发一个 feature,把离线作业放到容器里面跑。

自然而然的,需要在代码里使用 K8S 的 Java 客户端来与作业进行交互。

在技术选型的时候,发现了两个 Kubenetes 的 Java 客户端:

第一个是官方推出的 Java 客户端,但是 API 的使用体验巨差。随便举个例子:

 public class Example {
     public static void main(String[] args) throws IOException, ApiException{
         ApiClient client = Config.defaultClient();
         Configuration.setDefaultApiClient(client);
 
         CoreV1Api api = new CoreV1Api();
         V1PodList list = api.listPodForAllNamespaces(null, null, null, null, null, null, null, null, null);
         for (V1Pod item : list.getItems()) {
             System.out.println(item.getMetadata().getName());
         }
     }
 }

这是官方提供的一个 demo,可以看到 listPodForAllNamespaces 这个调用传递的参数列表,基本可以认为是通过某种协议生成的代码。

第二个客户端虽然非官方,但是 API 对于用户的友好性与官方库相比简直是天差地别。同样的代码:

 public class ListExamples {
 
   private static final Logger logger = LoggerFactory.getLogger(ListExamples.class);
 
   public static void main(String[] args) {
     String master = "https://192.168.42.20:8443/";
 
     Config config = new ConfigBuilder().withMasterUrl(master).build();
     try (final KubernetesClient client = new DefaultKubernetesClient(config)) {
 
       System.out.println(client.pods().list());
     } catch (KubernetesClientException e) {
       logger.error(e.getMessage(), e);
     }
   }
 
 }

所以在选型的时候选择了第二个客户端,这也是踩坑的开始。

2. 场景描述

实现在 K8S 运行作业的 feature 的过程中,遇到了这样一个场景:

服务进行重启之后,需要从 K8S 的 API Server 中恢复每一个作业的状态。

  • 作业是否已经创建了?

  • 作业是否正在运行?

  • 作业已经跑完了?

所以第一件事应该是根据 Job 的名字去 K8S 查询 Job 的状态。写下了如下代码:

   @Test
   public void jobTest() {
     final Job job = this.client.batch().jobs().withName("demo-data").fromServer().get();
     System.out.println(job);
   }

简单来看看最后 get 方法的具体内容:

   @Override
   public T get() {
     try {
       T answer = getMandatory();
       if (answer instanceof HasMetadata) {
         HasMetadata hasMetadata = (HasMetadata) answer;
         updateApiVersion(hasMetadata);
       } else if (answer instanceof KubernetesResourceList) {
         KubernetesResourceList list = (KubernetesResourceList) answer;
         updateApiVersion(list);
       }
       return answer;
     } catch (KubernetesClientException e) {
       if (e.getCode() != HttpURLConnection.HTTP_NOT_FOUND) {
         throw e;
       }
       return null;
     }
   }

从代码的内容来看,如果资源不存在 get 方法应该返回一个 null 值。与此相对的,还有一个 require 方法:

   @Override
   public T require() throws ResourceNotFoundException {
     try {
       T answer = getMandatory();
       if (answer == null) {
         throw new ResourceNotFoundException("The resource you request doesn't exist or couldn't be fetched.");
       }
       if (answer instanceof HasMetadata) {
         HasMetadata hasMetadata = (HasMetadata) answer;
         updateApiVersion(hasMetadata);
       } else if (answer instanceof KubernetesResourceList) {
         KubernetesResourceList list = (KubernetesResourceList) answer;
         updateApiVersion(list);
       }
       return answer;
     } catch (KubernetesClientException e) {
       if (e.getCode() != HttpURLConnection.HTTP_NOT_FOUND) {
         throw e;
       }
       throw new ResourceNotFoundException("Resource not found : " + e.getMessage());
     }
   }

如果不存在则会抛出一个 ResourceNotFoundException

 io.fabric8.kubernetes.client.KubernetesClientException: Failure executing: GET at: https://master01.dev.rack.xxxxx.com:6443/apis/batch/v1/namespaces/ares-worker/jobs/demo-data. Message: Forbidden! User ares-worker doesn't have permission. jobs.extensions "demo-data" is forbidden: User "system:serviceaccount:ares-worker:ares-worker" cannot get resource "jobs" in API group "extensions" in the namespace "ares-worker".

打个断点,可以看到如下情况:

image-20191023171256096

Response 的 code 竟然是 403!错误提示的内容表示 cannot get resource "jobs" in API group "extensions"

但是从 URL 来看使用的的确是 batch 的 API group,这个错误信息不符合事实啊!

3. 排查过程

除了问题,第一直觉应该是客户端的版本引用有问题。所以首先看一下 K8S 的版本:

 kubectl version
 Client Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.7", GitCommit:"8fca2ec50a6133511b771a11559e24191b1aa2b4", GitTreeState:"clean", BuildDate:"2019-09-18T14:47:22Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"darwin/amd64"}
 Server Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.3", GitCommit:"2d3c76f9091b6bec110a5e63777c332469e0cba2", GitTreeState:"clean", BuildDate:"2019-08-19T11:05:50Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"}

可以看到 Server 端的版本为 v1.15.3,而客户端的版本是 4.6.1。从 README 来看版本是完美匹配的啊!

那么是不是集群侧的权限设置有问题呢?答案是否定的,因为同样的代码在查询的确存在的 Job 时的确能正确返回。

无奈,只好开始断点 debug 大法,终于让我在 io.fabric8.kubernetes.client.utils.BackwardsCompatibilityInterceptor 这个类里面找到了答案:

   public Response intercept(Chain chain) throws IOException {
     Request request = chain.request();
     Response response = chain.proceed(request);
     if (!response.isSuccessful() && responseCodeToTransformations.keySet().contains(response.code())) {
       String url = request.url().toString();
       Matcher matcher = getMatcher(url);
       ResourceKey key = getKey(matcher);
       ResourceKey target = responseCodeToTransformations.get(response.code()).get(key);
       if (target != null) {
         response.close(); // At this point, we know we won't reuse or return the response; so close it to avoid a connection leak.
         String newUrl = new StringBuilder(url)
             .replace(matcher.start(API_VERSION), matcher.end(API_VERSION), target.version) // Order matters: We need to substitute right to left, so that former substitution don't affect the indexes of later.
             .replace(matcher.start(API_GROUP), matcher.end(API_GROUP), target.group)
             .toString();
 
         Request.Builder newRequest = request.newBuilder()
           .url(newUrl);
 
         Buffer buffer = new Buffer();
         if (request.body() != null && !request.method().equalsIgnoreCase(PATCH)) {
           request.body().writeTo(buffer);
             Object object = Serialization.unmarshal(buffer.inputStream());
           if (object instanceof HasMetadata) {
             HasMetadata h = (HasMetadata) object;
             h.setApiVersion(target.group + "/" + target.version);
             newRequest = newRequest.method(request.method(), RequestBody.create(OperationSupport.JSON, Serialization.asJson(h)));
           }
         }
 
         return chain.proceed(newRequest.build());
       }
     }
     return response;
   }

简单来说,如果 Kubenetes API Server 响应的 code 不是 200 并且在代码硬编码的某种配置中,会使用一个新的 URL 来请求。目的是实现向下的版本兼容性!

而这个新的 URL 采用了老版本的接口访问资源,所在的 API group 即为 extensions,导致返回了上面的错误信息。

4. 解决方案

查到了问题所在,解决就比较简单了:

  1. 自己构建客户端使用到 OkHttpClient,去掉这个拦截器。

  2. 在业务代码中通过匹配特定异常信息进行识别。

记一次物理机 OOM 的处理过程

1. 情景描述

1.1 技术背景

在之前开发的一个作业调度系统中,采用了一种最简单作业运行机制:

  • 物理机上启动称为 worker 的进程。

  • worker 进程与其他模块交互启动任务、停止任务、保存日志。

  • worker 运行作业的命令由调度模块生成派发。

  • 作业进程以 worker 进程的子进程的方式启动。

这种架构的优点是轻量级,任务的具体运行策略在运行时由调度中心决定,因此大量的特性可以在调度模块进行开发,worker 模块不需要频繁重启和更新。

但是缺点也同样明显,主要包括两个方面。

首先,因为物理机上没有做资源的隔离,所以所有作业共享物理机的资源,单个任务可能无限制地使用资源导致 worker 进程或者物理机的不稳定。

其次,作业都是由 worker 进程创建子进程的方式启动。一旦 worker 进程退出,原来作业进程的 ppid 都会变成 1。如果不对作业进程的信息进行持久化,那么重启之前的在运行的作业状态就会丢失,但是贸然重启又会导致同一个作业有两个实例同时运行。但即使持久化了作业进程的信息,在 worker 进程重启过程中运行结束的作业信息还是会丢失。

鉴于上述原因,针对当前的架构最重要的就是保证 worker 进程不会被意外终止。

1.2 问题描述

但是,就像墨菲定律所说:

如果事情有变坏的可能,不管这种可能性有多小,它总会发生。

最近频繁在半夜被报警电话打醒,worker 进程被 supervisor 重新拉起。只能先来一波令人窒息的运维操作,恢复这台物理机的状态,然后再排查问题的原因。

查看监控页面可知物理机的内存使用率和 CPU load 在短时间内一路走高,操作系统达到了 OOM 的边缘。

之前还发生过操作系统在类似的情况下直接重启的后果。

2. 排查手段

咨询了一下公司负责 SLA 的同事,初步判断时内核的 OOM-killer 将进程杀死导致了 worker 进程的重启。

2.1 OOM 的日志

首先 OOM-killer 发生作用时在内核的日志里是会有记录的:

  • /var/log/kern.log

  • /var/log/dmesg

    查找 Out of memory: Kill process 或者 Killed process 字样。

     sudo grep --color -n -B 30 'Killed process' /var/log/kern.log

    日志中会提示 OOM-killer 杀死进程前的内存使用信息、终止进程的 pid 以及 cmd 等。由于时间比较久远,相关日志被清除了,因此就不再这里贴出样例了。

2.2 OOM-killer 机制

根据下面几篇文章,可以简单了解一下 OOM-killer 机制的作用原理:

简单来说几个相关参数:

  • /proc/sys/vm/panic_on_oom

  • /proc/sys/vm/oom_dump_tasks

  • /proc/sys/vm/oom_kill_allocating_task

  • /proc/sys/vm/overcommit_memory

  • /proc/sys/vm/overcommit_ratio

  • /proc/[pid]/oom_score

  • /proc/[pid]/oom_adj

2.2.1 /proc/sys/vm/panic_on_oom

默认值是 0。

如果设置为 0,内核会杀死内存占用过多的进程。通常杀死内存占用最多的进程,系统就会恢复。

如果设置为 1,在发生 OOM 时,内核会 panic 。然而,如果一个进程通过内存策略或进程绑定限制了可以使用的节点,并且这些节点的内存已经耗尽,OOM-killer 可能会杀死一个进程来释放内存。在这种情况下,内核不会 panic,因为其他节点的内存可能还有空闲,这意味着整个系统的内存状况还没有处于崩溃状态。

如果设置为 2,在发生 OOM 时总是会强制 panic,即使在上面讨论的情况下也一样。即使在 memory cgroup 限制下发生的 OOM,整个系统也会 panic。

2.2.2 /proc/sys/vm/oom_dump_tasks

默认值是 1(启用),在内核执行OOM-killing时会打印系统内进程的信息(不包括内核线程),信息包括 pid、uid、tgid、vm size、rss、nr_ptes,swapents,oom_score_adj 和进程名称。

2.2.3 /proc/sys/vm/oom_kill_allocating_task

默认值是 0。如果设置为 0,OOM killer 会扫描进程列表,选择一个进程来杀死。通常都会选择消耗内存内存最多的进程,杀死这样的进程后可以释放大量的内存。如果设置为非零值,OOM killer 只会简单地将触发 OOM 的进程杀死,避免遍历进程列表(代价比较大)。如果 panic_on_oom 被设置,则会忽略 oom_kill_allocating_task 的值。

2.2.4 /proc/sys/vm/overcommit_memory

默认值为 0 。该参数有三个值,分别是:

  • 0:当用户空间请求更多的的内存时,内核尝试估算出剩余可用的内存。

  • 1:当设这个参数值为 1 时,内核允许超量使用内存直到用完为止。

  • 2:当设这个参数值为 2 时,内核会使用一个决不过量使用内存的算法,即系统整个内存地址空间不能超过 swap + 50% 的 RAM 值,50% 参数的设定是在 overcommit_ratio 中设定。

2.2.5 /proc/sys/vm/overcommit_ratio

默认值为:50。这个参数值只有在 vm.overcommit_memory=2 的情况下,这个参数才会生效。该值为物理内存比率,当 overcommit_memory=2 时,进程可使用的 swap 空间不可超过 PM * overcommit_ratio / 100

2.2.6 /proc/[pid]/oom_score

每一个进程都会有的一个值,在 OOM-killer 选择终止进程的时候,会首先终止 oom_score 最高的进程。

2.2.7 /proc/[pid]/oom_adj

/proc/[pid]/oom_adj ,该 pid 进程被 OOM-killer 杀掉的权重,介于 [-17,15] 之间,越高的权重,意味着更可能被 OOM-killer 选中,-17 表示禁止被 kill 掉。

oom_score_adj 的作用和 oom_adj 作用类似,如果设置为 -1000 则表示禁止被 kill 掉。

3. 解决方案

在当前场景下,其实可以接受单个作业被内核杀死而保全 worker 进程。因此要做的很简单,把 worker 进程的 oom_adj 调整为 -17 即可。

 sudo su - root
 echo -17 > /proc/[pid]/oom_adj

 

红烧羊排

  • 配料表:

    1. 羊排两斤

    2. 白萝卜半根

    3. 胡萝卜两根

    4. 生抽 8 汤匙

    5. 老抽 5 汤匙

    6. 干辣椒 8 颗

    7. 信牌卤料包 1 个

    8. 花椒若干

    9. 单晶冰糖 10 颗

  • 注意点:

    1. 羊排飞水,撇去浮沫加入花椒炖煮 1 小时。

    2. 捞出羊肉沥干水分,用剪刀沿着肋骨剪开。

    3. 在锅中加上适量花生油煸炒羊排去除油脂并抽去骨头。

    4. 将羊排加入铸铁锅内,注入热水没过羊肉。

    5. 加入卤料包、生抽和老抽大火煮开,加入冰糖和剪碎的干辣椒。

    6. 小火炖煮半小时。

    7. 胡萝卜和白萝卜滚刀切块加入锅中再次炖煮一个小时。

  • 经验总结:

    1. 羊排一共经历两次炖煮,第一次去血水也方便第二次炖入味。

    2. 羊排比较肥的话一定要用平底锅煸炒一下去油。

使用 Shadowsocks-libev 构建网络代理

1. 序

作为一个程序员,总有着一些科学上网的需求。面对纷繁的服务提供商,面对时断时续的网络服务,总还是觉得老话说的有道理:自己动手丰衣足食!

2. 搭建过程

搭建代理服务器的常见手段是在一台境外服务器上启动 Shadowsocks 服务,然后在终端使用客户端连接过去。大致过程可以分为几个简单步骤:

  1. 购买境外服务器。

  2. 搭建 Shadowsocks 服务。

  3. 下载客户端连接。

2.1 购买服务器

在同事的推荐下,在 Vultr1 购买相关的服务。配置和价格如下:

项目 规格
CPU 1 vCore
RAM 1024 M
Storage 25 G SSD
Bandwidth 1000 GB
Location Silicon Valley
OS Ubuntu 18.04
Price $ 5 /month

但就价格来说每年对应人民币在 400 元左右价格偏贵,但是如果考虑到同时能搭建 WordPress 和 Shadowsocks 服务的话相对更容易接受一些。

另外一个好处是灵活,如果这个 IP 被封了还可以在其他地区的机房搭建服务。

2.2 搭建服务

搭建服务只需要按步骤执行如下命令(默认 root 用户)2

 apt-get update
 apt-get install -y shadowsocks-libev

然后编辑配置文件 /etc/shadowsocks-libev/config.json

 {
     "server":"0.0.0.0",
     "server_port":<port>,
     "local_port":1080,
     "password":"<password>",
     "timeout":60,
     "method":"aes-256-cfb"
 }

这里 <server port><password> 根据喜好设置即可。

然后我们启动服务:

 # 重新启动 shadowsocks-libev (以防按照旧的配置启动了服务)
 systemctl restart shadowsocks-libev
 
 # 设置随着操作系统自启动
 systemctl enable shadowsocks-libev.service
 
 # 查看服务状态
 systemctl status shadowsocks-libev.service

另外针对网络拥塞的情况我们还可以使用 BBR 来提升网速,具体的方法为3

 echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
 echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
 sysctl -p

关于 BBR 的原理可参见参考资料4

2.3 下载客户端

Shadowsocks 的客户端可以从 Github 进行下载,具体页面为:

3. 参考资料

RestTemplate 与 Gzip Content-Encoding

1. 问题描述

最近做一个针对 Yarn Application 进行错误诊断的需求,需要从 Resource Manager 获取 Application 的运行信息,比如:

 GET /ws/v1/cluster/apps/application_1561545353229_936285 HTTP/1.1
 Host: yarn.xxx.com
 Accept: */*
 accept-encoding: gzip, deflate

这个接口使用 Postman 可以得到对应的结果:

 {
     "app": {
         "id": "application_1561545353229_936285",
         "user": "bp_growth",
         "name": "moses:278648",
         "queue": "root.bp_growth_dev",
         "state": "FINISHED",
         "finalStatus": "FAILED",
         "progress": 100,
         "trackingUI": "History",
         "trackingUrl": "http://yarn-rm02.tc.rack.xxx.com:8088/proxy/application_1561545353229_936285/",
         "diagnostics": "Task failed task_1561545353229_936285_m_000008\nJob failed as tasks failed. failedMaps:1 failedReduces:0\n",
         "clusterId": 1561545353229,
         "applicationType": "MAPREDUCE",
         "applicationTags": "",
         "startedTime": 1562740151124,
         "finishedTime": 1562740195218,
         "elapsedTime": 44094,
         "amContainerLogs": "http://data1117.tc.rack.xxx.com:8042/node/containerlogs/container_e93_1561545353229_936285_01_000001/bp_growth",
         "amHostHttpAddress": "data1117.tc.rack.xxx.com:8042",
         "allocatedMB": -1,
         "allocatedVCores": -1,
         "reservedMB": -1,
         "reservedVCores": -1,
         "runningContainers": -1,
         "memorySeconds": 1583688,
         "vcoreSeconds": 721,
         "preemptedResourceMB": 0,
         "preemptedResourceVCores": 0,
         "numNonAMContainerPreempted": 0,
         "numAMContainerPreempted": 0,
         "logAggregationStatus": "TIME_OUT"
     }
 }

但是使用 RestTemplate 去请求的时候却会得到异常:

 org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens; nested exception is com.fasterxml.jackson.core.JsonParseException: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens
  at [Source: (PushbackInputStream); line: 1, column: 2]

原因是 Response 的结果使用了 Gzip 进行压缩。

2. 解决方案

在网上找到的最简单的解决方案来自:How to parse gzip encoded response with RestTemplate from Spring-Web

简单来说就是在构造 RestTemplate 的时候指定 ClientHttpRequestFactory。

Maven 项目引入一个依赖:

 <dependency>
   <groupId>org.apache.httpcomponents</groupId>
   <artifactId>httpclient</artifactId>
 </dependency>

由于我同时使用了 SpringBoot,所以不需要指定版本。

具体使用时如:

   @Test
   public void test() {
 
     HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(
         HttpClientBuilder.create().build());
     RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
 
     Map forObject = restTemplate.getForObject("http://yarn.xxx.com/ws/v1/cluster/apps/application_1561545353229_936285", Map.class);
     System.out.println("forObject = " + forObject);
   }