本文是Kubernetes DNS-Based Service Discovery的翻译,也就是Kubernetes DNS specification的翻译,目前最新版本号是1.0.1。

0 - 关于此文档

本文档是Kubernetes基于DNS的服务发现的规范说明,尽管在Kubernetes中还有其他方式的服务发现协议和机制,但是DNS仍然是最常见而且是最推荐使用的扩展。实际的DNS服务并不一定由由默认的Kube-DNS提供。 本文档旨在为所有实现之间的通用性提供相应的基准。

阅读全文

在Docker环境中经常会遇到时区相关的问题,所以顺便也看了一下Linux系统下时间相关的一下配置和概念:

硬件时钟

硬件时钟也叫RTC(Real Time Clock)或者CMOS时钟,这个是保存在BIOS中的,仅能保存:年、月、日、时、分、秒这些时间数值,无法保存当前时区以及是否使用夏令时调节。

系统时钟

系统时钟也叫软件时钟,在系统时钟里是有时区等概念的,在Linux内核里,是保存为自 UTC 时间 1970 年1月1日经过的秒数。系统启动时会读取硬件时钟,并根据/etc/adjtime的设置计算当前的时钟。系统启动之后,系统时钟与硬件时钟独立运行,Linux 通过时钟中断计数维护系统时钟。

/etc/localtime

这个文件一般情况下是一个软链接,链接到/usr/share/zoneinfo/目录下的一个对应时区的二进制文件,比如设置Asia/Shanghai的时区,则/etc/localtime -> /usr/share/zoneinfo/Asia/Shanghai,调用date等工具获取时间时会考虑这个配置。
建议并强烈建议将这个文件设置为软链接,很多人会直接拷贝文件,其实是不推荐的。所有的Linux系统都会依赖这个文件。

/etc/timezone

这个文件一般会记录时区的直接文字表示,或者是一个时间偏移(很少见),比如如果设置时区为Asia/Shanghai,则这个文件的内容就会是Asia/Shanghai。这个文件并不是在所有Linux中都存在,比如在我的ArchLinux中就没有这个文件。这个文件一般也仅仅是一个简单的表示。

最后我们基本可以得到下面的结论:

  1. BIOS时间即硬件时间没有时区
  2. Linux在启动时会根据/etc/adjtime的设置和当前的硬件时间计算出当前的UTC时间,并将其和1970 年1月1日的秒差记录在内核中,由时钟中断继续维护。
  3. date等工具获得的时间是根据Linux内核中保存的时间和/etc/localtime的配置计算得来的。

其他相关问题,比如设置时间以及和Windows时间同步等可以参考下面的ArchLinux Wiki链接

参考:

  1. https://wiki.archlinux.org/index.php/Time
  2. https://unix.stackexchange.com/questions/384971/whats-the-difference-between-localtime-and-timezone-files

最近的一个项目需要用到Kubernetes的CronJob,主要用来定时执行一个备份任务,刚开始使用的时候发现没有按照预期的情况运行,所以决定看看CronJob Controller的代码,看看他是怎么实现对应的功能的,正好发现网上也没有其他人写过关于CronJob Controller代码的解析(可能是太简单了不用写吧)。所以也就正好记录一下。

CronJob Controller的代码在kubernetes/pkg/controller/cronjob路径下,主要的逻辑实现在这个目录的cronjob_controller.go,这里分析的是v1.10.2版本的代码,可以直接链接到Github查看。

阅读全文

之前一直听说Designing Data-Intensive Applications (DDIA) 这本书是神书,也决定读一下,顺便做些笔记,也算巩固一下学到的东西吧。

书的第一部分,主要关注于一些基础的知识,第一章的标题Reliable, Scalable, and Maintainable Applications就讲了三个当前应用的最主要特点:可靠性,可扩展性和可维护性。

可靠性

首先是Reliability也就是可靠性,主要包含下面的几个预期:

  • 应用可以按用户所期待的功能正常运行
  • 可以容忍用户犯的错误或者不正确的使用方式
  • 在对应的系统容量下性能能满足正常的使用要求
  • 能拒绝未授权或者滥用的情况

如果能满足上面的需求,可以说一个软件是可靠的,但是并不是所有的东西都能满足预期,当一些意料之外的东西发生了,称之为faults,系统正确应对这些faults的能力则称作fault-tolerant or resilient(即容错能力或者弹性),虽然容错能力很重要,但也不是意味着可以实现一个能容忍任何错误的系统(比如地球爆炸了)。
需要注意的是,faultfailure是不一样的,前者一般指的是系统某个部分没有按照设计正常工作,而后者一般就意味着整个系统都无法正常提供服务了。当然,我们没有办法去降低fault发生的概率,特别是降低到0,能做的,就是当fault发生时,系统不会因为这些faults变成failure状态,这也是一个容错系统的设计目标。
针对一个系统,我们可以人为的提升faults的发生概率,来验证系统的可靠性,比如kill某个进程,或者断开网络等等。一般情况下,比较严重的bug都是因为对错误的处理不完善导致的。
当然尽管我们可以通过设计容忍很多错误,但是预防错误的发生,远远比发生后再去修复来的好,毕竟很多错误是没办法被修复的,比如数据库被黑客入侵,这个操作无法被修复到原始的样子(数据已经泄漏)。
常见的错误主要有三个:

阅读全文

转自The complete guide to Go net/http timeouts

服务端超时

对于http.Server服务端有两个超时可以设置:ReadTimeoutWriteTimeout

1
2
3
4
5
srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
log.Println(srv.ListenAndServe())

各自的作用时间见图:

Server timeout

需要注意的是WriteTimeout被设置了两次,一次是在读取Http头过程中,另一次是在读取Http头结束后。

客户端超时

对于http.Client客户端,相对要复杂一点,一般的初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
c := &http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}

这些Timeout各自的作用时间见:

Client timeout

在CNI所以默认提供的Plugin中,bridge应该算是最简单的插件了,针对IPAM Plugin,最简单的应该是host-local,这两个插件也是Kubernetes网络kubenet需要的两个插件。所以这里看一下这两个插件的代码。

所有官方维护的代码,都开源在containernetworking/plugins项目中了。

其中bridge的代码在plugins/main/bridge目录,最重要的是cmdAddcmdDel两个函数,对应CNI SPEC中的ADD和DEL两个主要操作。主要来看一下cmdAdd的实现,精简(删除一些错误处理)后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
func cmdAdd(args *skel.CmdArgs) error {
n, cniVersion, err := loadNetConf(args.StdinData)
if n.IsDefaultGW {
n.IsGW = true
}
br, brInterface, err := setupBridge(n)
if err != nil {
return err
}
netns, err := ns.GetNS(args.Netns)
defer netns.Close()
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
if err != nil {
return err
}
result, err := current.NewResultFromResult(r)
if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}
result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}
gwsV4, gwsV6, err := calcGateways(result, n)
if err != nil {
return err
}
if err := netns.Do(func(_ ns.NetNS) error {
contVeth, err := net.InterfaceByName(args.IfName)
for _, ipc := range result.IPs {
if ipc.Version == "6" && (n.HairpinMode || n.PromiscMode) {
if err := disableIPV6DAD(args.IfName); err != nil {
return err
}
break
}
}
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
return err
}
for _, ipc := range result.IPs {
if ipc.Version == "4" {
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
}
}
return nil
}); err != nil {
return err
}
if n.IsGW {
var firstV4Addr net.IP
for _, gws := range []*gwInfo{gwsV4, gwsV6} {
for _, gw := range gws.gws {
if gw.IP.To4() != nil && firstV4Addr == nil {
firstV4Addr = gw.IP
}
err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
if err != nil {
return fmt.Errorf("failed to set bridge addr: %v", err)
}
}
if gws.gws != nil {
if err = enableIPForward(gws.family); err != nil {
return fmt.Errorf("failed to enable forwarding: %v", err)
}
}
}
}
if n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
return err
}
}
}
br, err = bridgeByName(n.BrName)
brInterface.Mac = br.Attrs().HardwareAddr.String()
result.DNS = n.DNS
return types.PrintResult(result, cniVersion)
}

阅读全文

本文主要介绍一下容器网络接口(CNI) 的SPEC,主要参考SPEC v0.3.1,以及目前最新的SPEC,目前新SPEC针对v0.3.1版本改动不是很大,特别是相关接口输入输出方面,因此可以看作是一样的。

总览

所有的CNI Plugin,都必须实现为可以被容器管理系统(如rtk、Kubernetes等)调用的可执行文件。
CNI插件负责将网络接口插入容器网络命名空间(例如veth pair的其中一端),并在主机上进行任何必要的改变(例如将veth pair的另一端连接到网桥)。然后应该将IP分配给接口,并通过调用适当的IPAM插件将与“IP地址管理”部分一致的IP地址分配给该网络接口,并设置好对应的路由。

参数

所有的CNI Plugin必须实现以下操作:

  • 添加一个容器到网络

    • Parameters:
      • Version. The version of CNI spec that the caller is using (container management system or the invoking plugin).
      • Container ID. A unique plaintext identifier for a container, allocated by the runtime. Must not be empty.
      • Network namespace path. This represents the path to the network namespace to be added, i.e. /proc/[pid]/ns/net or a bind-mount/link to it.
      • Network configuration. This is a JSON document describing a network to which a container can be joined. The schema is described below.
      • Extra arguments. This provides an alternative mechanism to allow simple configuration of CNI plugins on a per-container basis.
      • Name of the interface inside the container. This is the name that should be assigned to the interface created inside the container (network namespace); consequently it must comply with the standard Linux restrictions on interface names.
    • Result:
      • Interfaces list. Depending on the plugin, this can include the sandbox (eg, container or hypervisor) interface name and/or the host interface name, the hardware addresses of each interface, and details about the sandbox (if any) the interface is in.
      • IP configuration assigned to each interface. The IPv4 and/or IPv6 addresses, gateways, and routes assigned to sandbox and/or host interfaces.
      • DNS information. Dictionary that includes DNS information for nameservers, domain, search domains and options.
  • 从网络中删除一个容器

    • Parameters:
      • Version. The version of CNI spec that the caller is using (container management system or the invoking plugin).
      • Container ID, as defined above.
      • Network namespace path, as defined above.
      • Network configuration, as defined above.
      • Extra arguments, as defined above.
      • Name of the interface inside the container, as defined above.
    • All parameters should be the same as those passed to the corresponding add operation.
    • A delete operation should release all resources held by the supplied containerid in the configured network.
  • 报告插件支持的CNI版本

    • Parameters: NONE.
    • Result: information about the CNI spec versions supported by the plugin

      1
      2
      3
      4
      {
      "cniVersion": "0.3.1", // the version of the CNI spec in use for this output
      "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1" ] // the list of CNI spec versions that this plugin supports
      }

阅读全文

默认情况下,在使用rpmbuild打包时,会对安装的所有文件进行strip操作,去除文件的一些调试信息,并将这些调试信息放到debuginfo包中,但在很多时候,我们并不需要rpmbuild帮我们执行strip,也不需要生成debuginfo包,所以我们可以修改一下spec文件,关闭这些选项。

针对文件的strip操作是在__os_install_post这个宏中定义的,我们可以运行一下rpmbuild --showrc看一下原始的__os_install_post做了哪些操作:

1
2
3
4
5
6
7
8
9
10
...
-14: __os_install_post
/usr/lib/rpm/redhat/brp-compress
%{!?__debug_package:/usr/lib/rpm/redhat/brp-strip %{__strip}}
/usr/lib/rpm/redhat/brp-strip-static-archive %{__strip}
/usr/lib/rpm/redhat/brp-strip-comment-note %{__strip} %{__objdump}
/usr/lib/rpm/brp-python-bytecompile
/usr/lib/rpm/redhat/brp-python-hardlink
%{!?__jar_repack:/usr/lib/rpm/redhat/brp-java-repack-jars}
...

可以看到在打包时会对文件进行一系列操作,比如压缩,strip,编译Python脚本等,所以,我们只需要在spec文件中,加上%define __os_install_post %{nil},将__os_install_post设置为空,这样在打包的时候,就不会执行上面的这些操作了,也就不会对文件进行strip操作了。同样的,如果不需要生成debuginfo包,只需要再加上%define debug_package %{nil}就可以了。

这个问题是很久之前解决的,现在想起来,还是把之前的问题解决过程总结一下。
问题的起因是内部的一个Socket代理,用户对独享数据库的所有请求都需要经过这个Socket代理,某天一个用户反馈,切换到独享数据库之后,页面响应变得异常的慢,大概从1s左右直接到了60s左右,明显是有问题的,首先让用户开了xhprof看了一下,发现用户一个页面牵涉到了超过1000次SQL查询,这1000多次查询占据了绝大部分的时间,因为仅仅切换了数据库,所以问题的原因肯定还是数据库相关。
这个场景还是稍微有点特殊,一个页面里有超过1000次SQL查询的设计也不算合理,所以,我们就编写了测试用例,在PHP中,查询数据库1000次,测试直接连接数据库,和通过Socket代理连接数据库的情况:最后发现直连的速度非常快,但是过代理则慢的不可接受了,很明显是代理的问题。
于是我们尝试在代理机器上抓包分析一下:

tcpdump

其中10.67.15.102是我们Web运行环境的机器IP,10.67.15.212是Socket代理所在的机器10.13.144.139是数据库所在的机器,从id为22953的数据包开始,到22957,是一个SQL查询从Web运行环境到数据库的整个过程:

id为22953:运行环境102发送select语句到Socket代理212。数据包长度为296byte 时间:23.877515
id为22954:Socket代理212发送了一部分select语句128byte到数据库139。       时间:23.877611
id为22955:Socket代理212回运行环境103的ack。                             时间:23.917294
id为22956:数据库139回Socket代理212的ack。                               时间:23.918398
id为22957:Socket代理212发送剩余部分select语句168byte到数据库139。       时间:23.918415

阅读全文

一条syslog信息包含三部分,PRI, HEADER和MSG,其中PRI是<>扩起来的一个数字,这个数字就代表了不同Facility和Severity的消息。
其中Facility, Severity的数字代号列表如下:

Facility:

Code Keyword Description
0 kern kernel messages
1 user user-level messages
2 mail mail system
3 daemon system daemons
4 auth security/authorization messages
5 syslog messages generated internally by syslogd
6 lpr line printer subsystem (archaic subsystem)
7 news network news subsystem (archaic subsystem)
8 uucp UUCP subsystem (archaic subsystem)
9 clock daemon
10 authpriv security/authorization messages
11 ftp FTP daemon
12 - NTP subsystem
13 - log audit
14 - log alert
15 cron scheduling daemon
16 local0 local use 0 (local0)
17 local1 local use 1 (local1)
18 local2 local use 2 (local2)
19 local3 local use 3 (local3)
20 local4 local use 4 (local4)
21 local5 local use 5 (local5)
22 local6 local use 6 (local6)
23 local7 local use 7 (local7)

Severity:

Code Keyword Description
0 emerg System is unusable
1 alert Should be corrected immediately
2 crit Critical conditions
3 err Error conditions
4 warning May indicate that an error will occur if action is not taken.
5 notice Events that are unusual, but not error conditions.
6 info Normal operational messages that require no action.
7 debug Information useful to developers for debugging the application.

针对PRI的计算公式:PRI = FacilityCode*8 + SeverityCode
举个例子: local3.info的日志,它的PRI就是19*8+6=158,所以这条消息在传输中的格式为<158> {HEADER} {MEG}
再一个例子,如果看到一条PRI为14的消息,那么它实际的级别就是user.info (1*8+6=14)

参考:

  1. https://tools.ietf.org/html/rfc3164
  2. https://wiki.archlinux.org/index.php/systemd#Journal