np.ogrid 用法

np.ogrid 用法
np.ogrid 与 np.mgrid 方法相似,都是返回一个多维 meshgrid ,区别是前者返回开放式,后者返回密集式数组,数组中元素依据 np.arrange() 方法来生成, mgrid 与 ogrid 联系与区别
联系:
参数部分需传入一个列表,列表中若只存在一个元素 ,格式形如[a​ : b : step] (a表示起点,b 表示终点,step 表示步长,步长参数没有的时候默认为1),*终只生成一个数组;若列表中包含两个元[a : b : step,a1 : b1 : step1],*终生成两个数组;
区别
若列表中有两个元素时, np.mgrid *终返回两个ndarray,维度全部是 n*m ;生成的*个数组是依照列表中*个元素生成,对列进行扩展重复;另外一个数组是根据列表中第二个元素生成,对行进行扩展重复;
np.ogrid 也返回两个 ndarray ,只不过数组维度分别为 n *1 和 1 * m
>>> np.mgrid[1:20:5,0:8:1]
array([[[ 1,  1,  1,  1,  1,  1,  1,  1],
        [ 6,  6,  6,  6,  6,  6,  6,  6],
        [11, 11, 11, 11, 11, 11, 11, 11],
        [16, 16, 16, 16, 16, 16, 16, 16]],
       [[ 0,  1,  2,  3,  4,  5,  6,  7],
        [ 0,  1,  2,  3,  4,  5,  6,  7],
        [ 0,  1,  2,  3,  4,  5,  6,  7],
        [ 0,  1,  2,  3,  4,  5,  6,  7]]])
>>> np.ogrid[1:20:5,0:8:1]
[array([[ 1],
       [ 6],
       [11],
       [16]]), array([[0, 1, 2, 3, 4, 5, 6, 7]])]
以上是当步长为整数部分,当步长为 复数时,生成元素会以复数整数部分 n ,将元素终点到起点整数范围划分为 n 等份(生成元素中包含终点元素)
>>> ogrid[-1:1:5j]
array([-1. , -0.5,  0. ,  0.5,  1. ])

Python 批量修改文件后缀名

Python 批量修改文件后缀名
日常工作中,可能会有这样的需求:把一个文件夹下所有 jpg 图片转化为 png ;假设文件夹下只有4、5 张,手动修改的话是没问题,但如果数量达到上百张,就有点让人头疼,这个代码块正是用来解决此类问题的
import os
def batch_rename(work_dir, old_ext, new_ext):
    # files = os.listdir(work_dir)
    for filename in os.listdir(work_dir):
        # Get the file extension
        split_file = os.path.splitext(filename)
        # Unpack tuple element
        root_name, file_ext = split_file
        # Start of the logic to check the file extensions, if old_ext = file_ext
        if old_ext == file_ext:
            # Returns changed name of the file with new extention
            newfile = root_name + new_ext
            # Write the files
            os.rename(
                os.path.join(work_dir, filename),
                os.path.join(work_dir, newfile)
            )
    print(“rename is done!”)
    print(os.listdir(work_dir))

Python 解压压缩包至文件夹、把整个文件夹打包至压缩包

Python 解压压缩包至文件夹、把整个文件夹打包至压缩包
1,Python 解压压缩包至文件夹
import zipfile
with zipfile.ZipFile(path_to_zip_file, ‘r’) as zip_ref:
    zip_ref.extractall(directory_to_extract_to)
2,Python 把整个文件夹打包至压缩包
import os
import zipfile
def zipdir(path, ziph):
    # ziph is zipfile handle
    for root, dirs, files in os.walk(path):
        for file in files:
            ziph.write(os.path.join(root, file),
                       os.path.relpath(os.path.join(root, file),
                                       os.path.join(path, ‘..’)))
zipf = zipfile.ZipFile(‘Python.zip’, ‘w’, zipfile.ZIP_DEFLATED)
zipdir(‘tmp/’, zipf)
zipf.close()

Pytorch 中怎么计算网络的参数量

Pytorch 中怎么计算网络的参数量
本博文以 Dense Block 为例,Pytorch 为 DL 框架,*终计算模块参数量方法如下:
import torch
import torch.nn as nn
class Norm_Conv(nn.Module):
    def __init__(self,in_channel):
        super(Norm_Conv,self).__init__()
        self.layers = nn.Sequential(
            nn.Conv2d(in_channel,in_channel,3,1,1),
            nn.ReLU(True),
            nn.BatchNorm2d(in_channel),
            nn.Conv2d(in_channel,in_channel,3,1,1),
            nn.ReLU(True),
            nn.BatchNorm2d(in_channel),
            nn.Conv2d(in_channel,in_channel,3,1,1),
            nn.ReLU(True),
            nn.BatchNorm2d(in_channel))
    def forward(self,input):
        out = self.layers(input)
        return out
class DenseBlock_Norm(nn.Module):
    def __init__(self,in_channel):
        super(DenseBlock_Norm,self).__init__()
        self.first_layer = nn.Sequential(nn.Conv2d(in_channel,in_channel,3,1,1),
                                        nn.ReLU(True),
                                        nn.BatchNorm2d(in_channel))
        self.second_layer = nn.Sequential(nn.Conv2d(in_channel*2,in_channel,3,1,1),
                                          nn.ReLU(True),
                                          nn.BatchNorm2d(in_channel))
        self.third_layer = nn.Sequential(
            nn.Conv2d(in_channel*3,in_channel,3,1,1),
            nn.ReLU(True),
            nn.BatchNorm2d(in_channel))
    def forward(self,input):
        output1 = self.first_layer(input)
        output2 = self.second_layer(torch.cat((output1,input),dim=1))
        output3 = self.third_layer(torch.cat((input,output1,output2),dim=1))
        return output3
def count_param(model):
    param_count = 0
    for param in model.parameters():
        param_count += param.view(-1).size()[0]
    return param_count
# Get Parameter number of Network
in_channel = 128
net1 = Norm_Conv(in_channel)
print(‘Norm Conv parameter count is {}’.format(count_param(net1)))
net2 = DenseBlock_Norm(in_channel)
print(‘DenseBlock Norm parameter count is {}’.format(count_param(net2)))
*终结果如下
Norm Conv parameter count is 443520
DenseBlock Norm parameter count is 885888

SRE 是如何保障稳定性的

前言

在技术工作中,对于产品/基础技术研发和 SRE 两种角色,通常会有基于「是否侧重编码」的理解。对于产品研发转做 SRE ,经常会产生是否要「脱离编码工作」的看法,或者认为是否要「偏离对产品/基础技术的推进」。

基于过往的技术研发和稳定性保障的经验,分享个人对 SRE 的理解,探讨「面向产品/基础技术的研发」和「稳定性保障」两种角色之间的协作关系,更好地为业务服务。

%title插图%num

SRE 概述

*早讨论 SRE 来源于 Google 这本书《Site Reliability Engineering: How Google Runs Production Systems》。由 Google SRE 关键成员分享他们是如何对软件进行生命周期的整体性关注,以及为什么这样做能够帮助 Google 成功地构建、部署、监控和运维世界上现存*大的软件系统。

书的豆瓣链接:https://book.douban.com/subject/26875239/

从 wikipedia: Site reliability engineering(https://en.wikipedia.org/wiki/Site_reliability_engineering) 中可了解到 SRE 的定义:

Site reliability engineering (SRE) is a discipline that incorporates aspects of software engineering and applies them to infrastructure and operations problems. The main goals are to create scalable and highly reliable software systems.

其中有句形象描述 SRE 工作的描述:

SRE is “what happens when a software engineer is tasked with what used to be called operations.”

即 :

在 Google SRE 书中,对 SRE 日常工作状态有个准确的描述:至多 50% 的时间精力处理操作相关事宜,50% 以上的精力通过软件工程保障基础设施的稳定性和可扩展性。

基于上述描述,我对 SRE 的理解是:

  • 职责:保障基础设施的稳定性和可扩展性
  • 核心:解决问题
  • 方法:通过操作类事务积累问题经验,通过编码等方式提升问题的解决效率

%title插图%num

软件生命周期

Google SRE 一书中,对软件工程从生命周期角度有一个很形象的描述:

软件工程有的时候和养孩子类似:虽然生育的过程是痛苦和困难的,但是养育孩子成人的过程才是真正需要花费*大部分精力的地方。

一个软件系统的 40%~90% 的花销其实是花在开发建设完成之后不断维护过程中的。

项目生命周期中,设计和构建软件系统的时间精力占比,通常是少于系统上线之后的维护管理。为了更好地维护系统可靠运行,需要考虑两种类型的角色:

  • 专注于设计和构建软件系统
  • 专注于整个软件系统生命周期管理,包括从其设计到部署,历经不断改进,*后顺利下线

*类角色对应产品/基础技术研发,第二类角色对应 SRE,二者的共同目标均是为了达成项目目标,协同服务好业务。

%title插图%num

稳定性保障价值

针对稳定性的影响,直接参与处理客户问题的同学会更有体感:

  • 通过问题发生时客户直接反馈的影响程度、紧急程度,感受到稳定性给客户带来的焦虑
  • 通过问题处理结束后客户的反馈,感受到客户对稳定性保障的感谢或愤怒
  • 通过事后在营收状况、客户规模变化,感受到稳定性对业务营收的影响
  • 通过产品规划的的延期,感受到稳定性对产品迭代的影响

稳定性保障的价值由此凸显:

  • 保障客户的产品体验,满足客户对约定的可靠性诉求
  • 加速业务迭代,满足业务对稳定性诉求,业务注意力集中在更快速推出满足客户需求的功能

%title插图%num

SRE 如何保障稳定性

稳定性问题通常会有这些特征:

  • 人为导致,依赖专家经验
  • 一系列因素综合导致
  • 不可避免
  • 100% 保障没有必要

线上稳定性问题,人为操作不当导致的比例很高,集中在 发布 和 线上运维 两个环节,均是高频操作。对于复杂系统,这两个环节对专家经验有较强的依赖。

发生的稳定性问题通常具有系统性的特征,即非单个功能组件缺陷导致,而是由一系列因素综合作用导致,如缺少监控告警导致不能及时感知,缺少日志不能有助于快速定位问题,缺少良好的问题排查流程导致依赖个人能力,缺少良好的协调沟通而导致问题处理时长增加、客户影响程度加剧等。

问题是不可避免的,流量的突增、服务器/网络/存储的损坏、未覆盖的输入等,均会诱发问题的出现。

业务对外有 SLA,向客户承诺一定程度的稳定性,未达到时按照协议进行赔付,同时问题又不可不免,在满足内部 SLO 标准的前提下继续提升稳定性,会带来更高的实现成本,对业务的收益增量也会更小。

SRE 需要对问题特征有深入理解,系统性设计和实施解决方案,并抓住一段时间内的主要问题进行解决。

一种可参考的整体解决方案如下:

%title插图%num

落地过程中,可先从如下三个抓手系统解决:

  • 可控性
  • 可观测
  • 稳定性保障*佳实践

可控性方面,包括如下三个主要维度:

  • 发布管理
    • 重点解决发布导致的人为稳定性问题
    • 包括发布前重要变更评审和发布中变更动作管理等
  • 操作管理
    • 重点解决黑屏操作导致的人为稳定性问题
    • 包括统一集群操作入口、集群操作权限管理、集群操作审计等
  • 设计评审
    • 重点解决软件系统设计阶段应用稳定性保障*佳实践
    • 包括集群方案评审和重要功能设计评审等

可观测方面,包括如下几个重要维度:

  • 监控
    • 重点解决软件系统运行态的感知能力
    • 包括监控收集/可视化系统的搭建和维护等
  • 日志
    • 重点解决软件系统的问题可排查能力
    • 包括日志收集/存储/查询/分析系统的搭建和维护等
  • 巡检
    • 重点解决软件系统功能是否正常的主动探测能力
    • 包括巡检服务的搭建、通用巡检逻辑的开发维护等
  • 告警
    • 重点解决异常的及时触达需求
    • 包括告警系统的搭建、告警配置管理、告警途径管理、告警分析等

稳定性保障*佳实践,是从历史问题和业界实践方面抽象出意识、流程、规范、工具,在系统设计之初就融入其中,并在系统整个生命周期中加以使用,如通过模板固化*佳实践:

  • 项目质量验收标准
  • 项目安全生产标准
  • 项目发布前 checklist
  • 项目 TechReview 模板
  • 项目 Kick-off 模板
  • 项目管理规范
  • etc.

一个例子:

维度 评估项
可观测
  1. 是否提供了标准的 metrics API?
  2. 是否可以将日志持久化到日志系统?
  3. 是否配置了监控?
    1. 是否有对跌零因子的监控?
    2. 是否有服务降级的监控?
    3. 是否有限流的监控?
    4. 是否配置了对关键依赖方的可用性监控?
    5. 监控是否分级?
  1. 是否配置了告警?
    1. 是否有对跌零因子的告警?
    2. 是否有服务降级的告警?
    3. 是否有限流的告警?
    4. 是否配置了对关键依赖方的可用性告警?
    5. 告警是否分级?
  1. 是否可以配置巡检?
  2. 使用使用了 structured log 便于进行 log 的查询、分析?
可灰度
  1. 是否使用了具有灰度能力的 workload?
可回滚
  1. 是否使用了均有回滚能力的 workload?
  2. 组件是否进行了版本控制?
  3. 配置是否进行了版本控制?
可保护
  1. 是否有限流措施?
  2. 是否有降级措施?
  3. 是否配置了探针?
    1. 是否配置了 livenessProbe?
    2. 可被访问时,是否配置了 readinessProbe?
    3. 初始化耗时时,是否配置了 startupProbe?
  1. 是否有快速失败的能力?
    1. 是否有超时结束能力?
  1. 依赖方不可用时:
    1. 是否会持续对依赖方带来日常或更高压力?
    2. 是否会对上游带来反向压力?(如连接数、处理延时)
  1. 己方不可用时:
    1. 对上游的影响是否可控?
    2. 恢复时是否可以控制请求压力?
  1. 是否可以无损重建?
  2. 是否多副本部署?
  3. 是否配置了非亲和性?
  4. 是否跨 AZ 部署?
  5. 是否有处理预案
  6. 是否均有访问管理?
  7. 服务是否稳定性运行,是否会影响数据资产?
  8. 服务是否稳定性运行,是否会泄露敏感信息?
  9. 是否区分组件处于关键链路还是旁路?
  10. 是否有「爆炸半径」的整理?
  11. 是否有「跌零因子」的整理?
可控成本
  1. 是否配置了 limit resources?
  2. 变更是增加还是降低用户成本?
  3. 变更是增加还是降低平台成本?
易于运维
  1. 是否可以做到变更配置时无需重建实例?
  2. 是否有白屏化运维途径?
  3. 是否有「端到端管控链路」流程图
  4. 是否有「端到端数据链路」流程图

为了便于理解,可以再针对 check 项形成分级,便于交流和进行项目稳定性评估:

级别 标准
L0
  • 可观测、可灰度、可回滚 均不满足
L1
  • 可观测、可灰度、可回滚 满足 50% 以上要求
L2
  • 可观测、可灰度、可回滚 满足 90% 以上要求
L3
  • 可观测、可灰度、可回滚 满足 90% 以上要求
  • 可保护满足 50% 以上要求
L4
  • 可观测、可灰度、可回滚 满足 90% 以上要求
  • 可保护满足 90% 以上要求
  • 可控成本满足 50% 以上要求
L5
  • 可观测、可灰度、可回滚 满足 90% 以上要求
  • 可保护满足 90% 以上要求
  • 可控成本满足 90% 以上要求

当*佳实践可以通过文档进行规范化,接下来就可以提供工具或服务将其低成本应用,使得稳定性保障*佳实践成为基础设施。

SRE 需要在稳定性相关的方法论和实践方面不断迭代,自上而下设计,自下而上反馈,合理、可靠保障稳定性。

%title插图%num

共赢,携手服务业务

再回顾下软件系统生命周期中的两类角色:

  • 产品/基础技术研发:专注于设计和构建软件系统
  • SRE:专注于整个软件系统生命周期管理,包括从其设计到部署,历经不断改进,*后顺利下线

这两类角色是相互协作、相互服务的关系,拥有共同的目标:满足业务需求,更好服务业务。

SRE 通常会横向支撑多个项目,对线上问题的类型、解决实践有更为全面的理解和思考,基于此会形成*佳实践的理论、工具或服务,为研发提供理论、工具的支持,也可以在此基础上产品化稳定性保障解决方案,为更多的客户服务,创造更大的价值。

产品/基础技术研发对业务需求、功能/技术细节有更深入的理解,一方面直接带来业务价值,一方面可通过实践为稳定性保障带来切合实际的需求,进一步和 SRE 共同保障稳定性。

两种类型的角色,需要朝着共同的目标并肩协作,与业务共同发展,实现共赢。

CPU 是如何执行任务的?

前言

关于 CPU ,你清楚下面这几个问题吗?

  • 有了内存,为什么还需要 CPU Cache?
  • CPU 是怎么读写数据的?
  • 如何让 CPU 能读取数据更快一些?
  • CPU 伪共享是如何发生的?又该如何避免?
  • CPU 是如何调度任务的?如果你的任务对响应要求很高,你希望它总是能被先调度,这该怎么办?

这篇,我们就来回答这些问题。

%title插图%num

%title插图%num

CPU 如何读写数据?

先来认识 CPU 的架构,只有理解了 CPU 的 架构,才能更好地理解 CPU 是如何读写数据的,对于现代 CPU 的架构图如下:

%title插图%num

可以看到,一个 CPU 里通常会有多个 CPU 核心,比如上图中的 1 号和 2 号 CPU 核心,并且每个 CPU 核心都有自己的 L1 Cache 和 L2 Cache,而 L1 Cache 通常分为 dCache(数据缓存) 和 iCache(指令缓存),L3 Cache 则是多个核心共享的,这就是 CPU 典型的缓存层次。

上面提到的都是 CPU 内部的 Cache,放眼外部的话,还会有内存和硬盘,这些存储设备共同构成了金字塔存储层次。如下图所示:

%title插图%num

从上图也可以看到,从上往下,存储设备的容量会越大,而访问速度会越慢。至于每个存储设备的访问延时,你可以看下图的表格:

%title插图%num

你可以看到, CPU 访问 L1 Cache 速度比访问内存快 100 倍,这就是为什么 CPU 里会有 L1~L3 Cache 的原因,目的就是把 Cache 作为 CPU 与内存之间的缓存层,以减少对内存的访问频率。

CPU 从内存中读取数据到 Cache 的时候,并不是一个字节一个字节读取,而是一块一块的方式来读取数据的,这一块一块的数据被称为 CPU Line(缓存行),所以 CPU Line 是 CPU 从内存读取数据到 Cache 的单位

至于 CPU Line 大小,在 Linux 系统可以用下面的方式查看到,你可以看我服务器的 L1 Cache Line 大小是 64 字节,也就意味着 L1 Cache 一次载入数据的大小是 64 字节

%title插图%num

那么对数组的加载, CPU 就会加载数组里面连续的多个数据到 Cache 里,因此我们应该按照物理内存地址分布的顺序去访问元素,这样访问数组元素的时候,Cache 命中率就会很高,于是就能减少从内存读取数据的频率, 从而可提高程序的性能。

但是,在我们不使用数组,而是使用单独的变量的时候,则会有 Cache 伪共享的问题,Cache 伪共享问题上是一个性能杀手,我们应该要规避它。

接下来,就来看看 Cache 伪共享是什么?又如何避免这个问题?

现在假设有一个双核心的 CPU,这两个 CPU 核心并行运行着两个不同的线程,它们同时从内存中读取两个不同的数据,分别是类型为 long 的变量 A 和 B,这个两个数据的地址在物理内存上是连续的,如果 Cahce Line 的大小是 64 字节,并且变量 A 在 Cahce Line 的开头位置,那么这两个数据是位于同一个 Cache Line 中,又因为 CPU Line 是 CPU 从内存读取数据到 Cache 的单位,所以这两个数据会被同时读入到了两个 CPU 核心中各自 Cache 中。

%title插图%num

我们来思考一个问题,如果这两个不同核心的线程分别修改不同的数据,比如 1 号 CPU 核心的线程只修改了 变量 A,或 2 号 CPU 核心的线程的线程只修改了变量 B,会发生什么呢?

  分析伪共享的问题

现在我们结合保证多核缓存一致的 MESI 协议,来说明这一整个的过程,如果你还不知道 MESI 协议,你可以看我这篇文章「10 张图打开 CPU 缓存一致性的大门」。

①  *开始变量 A 和 B 都还不在 Cache 里面,假设 1 号核心绑定了线程 A,2 号核心绑定了线程 B,线程 A 只会读写变量 A,线程 B 只会读写变量 B。

%title插图%num

②  1 号核心读取变量 A,由于 CPU 从内存读取数据到 Cache 的单位是 Cache Line,也正好变量 A 和 变量 B 的数据归属于同一个 Cache Line,所以 A 和 B 的数据都会被加载到 Cache,并将此 Cache Line 标记为「独占」状态。

%title插图%num

③  接着,2 号核心开始从内存里读取变量 B,同样的也是读取 Cache Line 大小的数据到 Cache 中,此 Cache Line 中的数据也包含了变量 A 和 变量 B,此时 1 号和 2 号核心的 Cache Line 状态变为「共享」状态。

%title插图%num

④  1 号核心需要修改变量 A,发现此 Cache Line 的状态是「共享」状态,所以先需要通过总线发送消息给 2 号核心,通知 2 号核心把 Cache 中对应的 Cache Line 标记为「已失效」状态,然后 1 号核心对应的 Cache Line 状态变成「已修改」状态,并且修改变量 A。

%title插图%num

⑤ 之后,2 号核心需要修改变量 B,此时 2 号核心的 Cache 中对应的 Cache Line 是已失效状态,另外由于 1 号核心的 Cache 也有此相同的数据,且状态为「已修改」状态,所以要先把 1 号核心的 Cache 对应的 Cache Line 写回到内存,然后 2 号核心再从内存读取 Cache Line 大小的数据到 Cache 中,*后把变量 B 修改到 2 号核心的 Cache 中,并将状态标记为「已修改」状态。

%title插图%num

所以,可以发现如果 1 号和 2 号 CPU 核心这样持续交替的分别修改变量 A 和 B,就会重复 ④ 和 ⑤ 这两个步骤,Cache 并没有起到缓存的效果,虽然变量 A 和 B 之间其实并没有任何的关系,但是因为同时归属于一个 Cache Line ,这个 Cache Line 中的任意数据被修改后,都会相互影响,从而出现 ④ 和 ⑤ 这两个步骤。

因此,这种因为多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing

  避免伪共享的方法

因此,对于多个线程共享的热点数据,即经常会修改的数据,应该避免这些数据刚好在同一个 Cache Line 中,否则就会出现为伪共享的问题。

接下来,看看在实际项目中是用什么方式来避免伪共享的问题的。

在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题。

%title插图%num

从上面的宏定义,我们可以看到:

  • 如果在多核(MP)系统里,该宏定义是 __cacheline_aligned,也就是 Cache Line 的大小;
  • 而如果在单核系统里,该宏定义是空的;

因此,针对在同一个 Cache Line 中的共享的数据,如果在多核之间竞争比较严重,为了防止伪共享现象的发生,可以采用上面的宏定义使得变量在 Cache Line 里是对齐的。

举个例子,有下面这个结构体:

%title插图%num

结构体里的两个成员变量 a 和 b 在物理内存地址上是连续的,于是它们可能会位于同一个 Cache Line 中,如下图:

%title插图%num

所以,为了防止前面提到的 Cache 伪共享问题,我们可以使用上面介绍的宏定义,将 b 的地址设置为 Cache Line 对齐地址,如下:

%title插图%num

这样 a 和 b 变量就不会在同一个 Cache Line 中了,如下图:

%title插图%num

所以,避免 Cache 伪共享实际上是用空间换时间的思想,浪费一部分 Cache 空间,从而换来性能的提升。

我们再来看一个应用层面的规避方案,有一个 Java 并发框架 Disruptor 使用「字节填充 + 继承」的方式,来避免伪共享的问题。

Disruptor 中有一个 RingBuffer 类会经常被多个线程使用,代码如下:

%title插图%num

你可能会觉得 RingBufferPad 类里 7 个 long 类型的名字很奇怪,但事实上,它们虽然看起来毫无作用,但却对性能的提升起到了至关重要的作用。

我们都知道,CPU Cache 从内存读取数据的单位是 CPU Line,一般 64 位 CPU 的 CPU Line 的大小是 64 个字节,一个 long 类型的数据是 8 个字节,所以 CPU 一下会加载 8 个 long 类型的数据。

根据 JVM 对象继承关系中父类成员和子类成员,内存地址是连续排列布局的,因此 RingBufferPad 中的 7 个 long 类型数据作为 Cache Line 前置填充,而 RingBuffer 中的 7 个 long 类型数据则作为 Cache Line 后置填充,这 14 个 long 变量没有任何实际用途,更不会对它们进行读写操作。

%title插图%num

另外,RingBufferFelds 里面定义的这些变量都是 final 修饰的,意味着*次加载之后不会再修改, 又由于「前后」各填充了 7 个不会被读写的 long 类型变量,所以无论怎么加载 Cache Line,这整个 Cache Line 里都没有会发生更新操作的数据,于是只要数据被频繁地读取访问,就自然没有数据被换出 Cache 的可能,也因此不会产生伪共享的问题

%title插图%num

CPU 如何选择线程?

了解完 CPU 读取数据的过程后,我们再来看看 CPU 是根据什么来选择当前要执行的线程。

在 Linux 内核中,进程和线程都是用 tark_struct 结构体表示的,区别在于线程的 tark_struct 结构体里部分资源是共享了进程已创建的资源,比如内存地址空间、代码段、文件描述符等,所以 Linux 中的线程也被称为轻量级进程,因为线程的 tark_struct 相比进程的 tark_struct 承载的 资源比较少,因此以「轻」得名。

一般来说,没有创建线程的进程,是只有单个执行流,它被称为是主线程。如果想让进程处理更多的事情,可以创建多个线程分别去处理,但不管怎么样,它们对应到内核里都是 tark_struct

%title插图%num

所以,Linux 内核里的调度器,调度的对象就是 tark_struct,接下来我们就把这个数据结构统称为任务

在 Linux 系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:

  • 实时任务,对系统的响应时间要求很高,也就是要尽可能快的执行实时任务,优先级在 0~99 范围内的就算实时任务;
  • 普通任务,响应时间没有很高的要求,优先级在 100~139 范围内都是普通任务级别;

  调度类

由于任务有优先级之分,Linux 系统为了保障高优先级的任务能够尽可能早的被执行,于是分为了这几种调度类,如下图:

%title插图%num

Deadline 和 Realtime 这两个调度类,都是应用于实时任务的,这两个调度类的调度策略合起来共有这三种,它们的作用如下:

  • SCHED_DEADLINE:是按照 deadline 进行调度的,距离当前时间点*近的 deadline 的任务会被优先调度;
  • SCHED_FIFO:对于相同优先级的任务,按先来先服务的原则,但是优先级更高的任务,可以抢占低优先级的任务,也就是优先级高的可以「插队」;
  • SCHED_RR:对于相同优先级的任务,轮流着运行,每个任务都有一定的时间片,当用完时间片的任务会被放到队列尾部,以保证相同优先级任务的公平性,但是高优先级的任务依然可以抢占低优先级的任务;

而 Fair 调度类是应用于普通任务,都是由 CFS 调度器管理的,分为两种调度策略:

  • SCHED_NORMAL:普通任务使用的调度策略;
  • SCHED_BATCH:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低它的优先级。

  完全公平调度

我们平日里遇到的基本都是普通任务,对于普通任务来说,公平性*重要,在 Linux 里面,实现了一个基于 CFS 的调度算法,也就是完全公平调度(Completely Fair Scheduling

这个算法的理念是想让分配给每个任务的 CPU 时间是一样,于是它为每个任务安排一个虚拟运行时间 vruntime,如果一个任务在运行,其运行的越久,该任务的 vruntime 自然就会越大,而没有被运行的任务,vruntime 是不会变化的。

那么,在 CFS 算法调度的时候,会优先选择 vruntime 少的任务,以保证每个任务的公平性。

这就好比,让你把一桶的奶茶平均分到 10 杯奶茶杯里,你看着哪杯奶茶少,就多倒一些;哪个多了,就先不倒,这样经过多轮操作,虽然不能保证每杯奶茶完全一样多,但至少是公平的。

当然,上面提到的例子没有考虑到优先级的问题,虽然是普通任务,但是普通任务之间还是有优先级区分的,所以在计算虚拟运行时间 vruntime 还要考虑普通任务的权重值,注意权重值并不是优先级的值,内核中会有一个 nice 级别与权重值的转换表,nice 级别越低的权重值就越大,至于 nice 值是什么,我们后面会提到。
于是就有了以下这个公式:

%title插图%num

你可以不用管 NICE_0_LOAD 是什么,你就认为它是一个常量,那么在「同样的实际运行时间」里,高权重任务的 vruntime 比低权重任务的 vruntime ,你可能会奇怪为什么是少的?你还记得 CFS 调度吗,它是会优先选择 vruntime 少的任务进行调度,所以高权重的任务就会被优先调度了,于是高权重的获得的实际运行时间自然就多了。

  CPU 运行队列

一个系统通常都会运行着很多任务,多任务的数量基本都是远超 CPU 核心数量,因此这时候就需要排队

事实上,每个 CPU 都有自己的运行队列(Run Queue, rq,用于描述在此 CPU 上所运行的所有进程,其队列包含三个运行队列,Deadline 运行队列 dl_rq、实时任务运行队列 rt_rq 和 CFS 运行队列 csf_rq,其中 csf_rq 是用红黑树来描述的,按 vruntime 大小来排序的,*左侧的叶子节点,就是下次会被调度的任务。

%title插图%num

这几种调度类是有优先级的,优先级如下:Deadline > Realtime > Fair,这意味着 Linux 选择下一个任务执行的时候,会按照此优先级顺序进行选择,也就是说先从 dl_rq 里选择任务,然后从 rt_rq 里选择任务,*后从 csf_rq 里选择任务。因此,实时任务总是会比普通任务优先被执行

  调整优先级

如果我们启动任务的时候,没有特意去指定优先级的话,默认情况下都是普通任务,普通任务的调度类是 Fail,由 CFS 调度器来进行管理。CFS 调度器的目的是实现任务运行的公平性,也就是保障每个任务的运行的时间是差不多的。

如果你想让某个普通任务有更多的执行时间,可以调整任务的 nice 值,从而让优先级高一些的任务执行更多时间。nice 的值能设置的范围是 -20~19, 值越低,表明优先级越高,因此 -20 是*高优先级,19 则是*低优先级,默认优先级是 0。

是不是觉得 nice 值的范围很诡异?事实上,nice 值并不是表示优先级,而是表示优先级的修正数值,它与优先级(priority)的关系是这样的:priority(new) = priority(old) + nice。内核中,priority 的范围是 0~139,值越低,优先级越高,其中前面的 0~99 范围是提供给实时任务使用的,而 nice 值是映射到 100~139,这个范围是提供给普通任务用的,因此 nice 值调整的是普通任务的优先级。

%title插图%num

在前面我们提到了,权重值与 nice 值的关系的,nice 值越低,权重值就越大,计算出来的 vruntime 就会越少,由于 CFS 算法调度的时候,就会优先选择 vruntime 少的任务进行执行,所以 nice 值越低,任务的优先级就越高。

我们可以在启动任务的时候,可以指定 nice 的值,比如将 mysqld 以 -3 优先级:

%title插图%num

如果想修改已经运行中的任务的优先级,则可以使用 renice 来调整 nice 值:

%title插图%num

nice 调整的是普通任务的优先级,所以不管怎么缩小 nice 值,任务永远都是普通任务,如果某些任务要求实时性比较高,那么你可以考虑改变任务的优先级以及调度策略,使得它变成实时任务,比如:

%title插图%num

%title插图%num

总结

理解 CPU 是如何读写数据的前提,是要理解 CPU 的架构,CPU 内部的多个 Cache + 外部的内存和磁盘都就构成了金字塔的存储器结构,在这个金字塔中,越往下,存储器的容量就越大,但访问速度就会小。

CPU 读写数据的时候,并不是按一个一个字节为单位来进行读写,而是以 CPU Line 大小为单位,CPU Line 大小一般是 64 个字节,也就意味着 CPU 读写数据的时候,每一次都是以 64 字节大小为一块进行操作。

因此,如果我们操作的数据是数组,那么访问数组元素的时候,按内存分布的地址顺序进行访问,这样能充分利用到 Cache,程序的性能得到提升。但如果操作的数据不是数组,而是普通的变量,并在多核 CPU 的情况下,我们还需要避免 Cache Line 伪共享的问题。

所谓的 Cache Line 伪共享问题就是,多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象。那么对于多个线程共享的热点数据,即经常会修改的数据,应该避免这些数据刚好在同一个 Cache Line 中,避免的方式一般有 Cache Line 大小字节对齐,以及字节填充等方法。

系统中需要运行的多线程数一般都会大于 CPU 核心,这样就会导致线程排队等待 CPU,这可能会产生一定的延时,如果我们的任务对延时容忍度很低,则可以通过一些人为手段干预 Linux 的默认调度策略和优先级。

关于容器底层,超详细的 Namespace 机制讲解

Namespace

Linux Namespace 是 Linux 提供的一种内核级别环境隔离的方法。这种隔离机制和 chroot 很类似,chroot 是把某个目录修改为根目录,从而无法访问外部的内容。Linux Namesapce 在此基础之上,提供了对 UTS、IPC、Mount、PID、Network、User 等的隔离机制,如下所示。

分类 系统调用参数 相关内核版本
Mount Namespaces CLONE_NEWNS Linux 2.4.19
UTS Namespaces CLONE_NEWUTS Linux 2.6.19
IPC Namespaces CLONE_NEWIPC Linux 2.6.19
PID Namespaces CLONE_NEWPID Linux 2.6.19
Network Namespaces CLONE_NEWNET 始于Linux 2.6.24 完成于 Linux 2.6.29
User Namespaces CLONE_NEWUSER 始于 Linux 2.6.23 完成于 Linux 3.8)

Linux Namespace 官方文档:Namespaces in operation

namespace 有三个系统调用可以使用:

  • clone() — 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。
  • unshare() — 使某个进程脱离某个 namespace
  • setns(int fd, int nstype) — 把某进程加入到某个 namespace

下面使用这几个系统调用来演示 Namespace 的效果,更加详细地可以看 DOCKER基础技术:LINUX NAMESPACE(上)、 DOCKER基础技术:LINUX NAMESPACE(下)。

%title插图%num

UTS Namespace

UTS Namespace 主要是用来隔离主机名的,也就是每个容器都有自己的主机名。我们使用如下的代码来进行演示。注意:假如在容器内部没有设置主机名的话会使用主机的主机名的;假如在容器内部设置了主机名但是没有使用 CLONE_NEWUTS 的话那么改变的其实是主机的主机名。

  1. #define _GNU_SOURCE
  2. #include <sys/types.h>
  3. #include <sys/wait.h>
  4. #include <sys/mount.h>
  5. #include <stdio.h>
  6. #include <sched.h>
  7. #include <signal.h>
  8. #include <unistd.h>
  9. #define STACK_SIZE (1024 * 1024)
  10. static char container_stack[STACK_SIZE];
  11. char* const container_args[] = {
  12. “/bin/bash”,
  13. NULL
  14. };
  15. int container_main(void* arg) {
  16. printf(“Container [%5d] – inside the container!\n”, getpid());
  17. sethostname(“container_dawn”, 15);
  18. execv(container_args[0], container_args);
  19. printf(“Something’s wrong!\n”);
  20. return 1;
  21. }
  22. int main() {
  23. printf(“Parent [%5d] – start a container!\n”, getpid());
  24. int container_id = clone(container_main, container_stack + STACK_SIZE,
  25. CLONE_NEWUTS | SIGCHLD, NULL);
  26. waitpid(container_id, NULL, 0);
  27. printf(“Parent – container stopped!\n”);
  28. return 0;
  29. }

%title插图%num

%title插图%num

PID Namespace

每个容器都有自己的进程环境中,也就是相当于容器内进程的 PID 从 1 开始命名,此时主机上的 PID 其实也还是从 1 开始命名的,就相当于有两个进程环境:一个主机上的从 1 开始,另一个容器里的从 1 开始。

为啥 PID 从 1 开始就相当于进程环境的隔离了呢?因此在传统的 UNIX 系统中,PID 为 1 的进程是 init,地位特殊。它作为所有进程的父进程,有很多特权。另外,其还会检查所有进程的状态,我们知道如果某个进程脱离了父进程(父进程没有 wait 它),那么 init 就会负责回收资源并结束这个子进程。所以要想做到进程的隔离,首先需要创建出 PID 为 1 的进程。

但是,【kubernetes 里面的话】

  1. int container_main(void* arg) {
  2. printf(“Container [%5d] – inside the container!\n”, getpid());
  3. sethostname(“container_dawn”, 15);
  4. execv(container_args[0], container_args);
  5. printf(“Something’s wrong!\n”);
  6. return 1;
  7. }
  8. int main() {
  9. printf(“Parent [%5d] – start a container!\n”, getpid());
  10. int container_id = clone(container_main, container_stack + STACK_SIZE,
  11. CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL);
  12. waitpid(container_id, NULL, 0);
  13. printf(“Parent – container stopped!\n”);
  14. return 0;
  15. }

%title插图%num

如果此时你在子进程的 shell 中输入 ps、top 等命令,我们还是可以看到所有进程。这是因为,ps、top 这些命令是去读 /proc 文件系统,由于此时文件系统并没有隔离,所以父进程和子进程通过命令看到的情况都是一样的。

%title插图%num

IPC Namespace

常见的 IPC 有共享内存、信号量、消息队列等。当使用 IPC Namespace 把 IPC 隔离起来之后,只有同一个 Namespace 下的进程才能相互通信,因为主机的 IPC 和其他 Namespace 中的 IPC 都是看不到了的。而这个的隔离主要是因为创建出来的 IPC 都会有一个唯一的 ID,那么主要对这个 ID 进行隔离就好了。

想要启动 IPC 隔离,只需要在调用 clone 的时候加上 CLONE_NEWIPC 参数就可以了。

  1. int container_main(void* arg) {
  2. printf(“Container [%5d] – inside the container!\n”, getpid());
  3. sethostname(“container_dawn”, 15);
  4. execv(container_args[0], container_args);
  5. printf(“Something’s wrong!\n”);
  6. return 1;
  7. }
  8. int main() {
  9. printf(“Parent [%5d] – start a container!\n”, getpid());
  10. int container_id = clone(container_main, container_stack + STACK_SIZE,
  11. CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | SIGCHLD, NULL);
  12. waitpid(container_id, NULL, 0);
  13. printf(“Parent – container stopped!\n”);
  14. return 0;
  15. }

%title插图%num

Mount Namespace

Mount Namespace 可以让容器有自己的 root 文件系统。需要注意的是,在通过 CLONE_NEWNS 创建 mount namespace 之后,父进程会把自己的文件结构复制给子进程中。所以当子进程中不重新 mount 的话,子进程和父进程的文件系统视图是一样的,假如想要改变容器进程的视图,一定需要重新 mount(这个是 mount namespace  和其他 namespace 不同的地方)。

另外,子进程中新的 namespace 中的所有 mount 操作都只影响自身的文件系统(注意这边是 mount 操作,而创建文件等操作都是会有所影响的),而不对外界产生任何影响,这样可以做到比较严格地隔离(当然这边是除 share mount 之外的)。

下面我们重新挂载子进程的 /proc 目录,从而可以使用 ps 来查看容器内部的情况。

  1. int container_main(void* arg) {
  2. printf(“Container [%5d] – inside the container!\n”, getpid());
  3. sethostname(“container_dawn”, 15);
  4. if (mount(“proc”, “/proc”, “proc”, 0, NULL) !=0 ) {
  5. perror(“proc”);
  6. }
  7. execv(container_args[0], container_args);
  8. printf(“Something’s wrong!\n”);
  9. return 1;
  10. }
  11. int main() {
  12. printf(“Parent [%5d] – start a container!\n”, getpid());
  13. int container_id = clone(container_main, container_stack + STACK_SIZE,
  14. CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
  15. waitpid(container_id, NULL, 0);
  16. printf(“Parent – container stopped!\n”);
  17. return 0;
  18. }

%title插图%num

这里会有个问题就是在退出子进程之后,当再次使用 ps -elf 的时候会报错,如下所示

%title插图%num

这是因为 /proc 是 share mount,对它的操作会影响所有的 mount namespace,可以看这里:http://unix.stackexchange.com/questions/281844/why-does-child-with-mount-namespace-affect-parent-mounts

上面仅仅重新 mount 了 /proc 这个目录,其他的目录还是跟父进程一样视图的。一般来说,容器创建之后,容器进程需要看到的是一个独立的隔离环境,而不是继承宿主机的文件系统。接下来演示一个山寨镜像,来模仿 Docker 的 Mount Namespace。也就是给子进程实现一个较为完整的独立的 root 文件系统,让这个进程只能访问自己构成的文件系统中的内容(想想我们平常使用 Docker 容器的样子)。

  • 首先我们使用 docker export 将 busybox 镜像导出成一个 rootfs 目录,这个 rootfs 目录的情况如图所示,已经包含了 /proc/sys 等特殊的目录。

    %title插图%num

  • 之后我们在代码中将一些特殊目录重新挂载,并使用 chroot() 系统调用将进程的根目录改成上文的 rootfs 目录。
    1. char* const container_args[] = {
    2. “/bin/sh”,
    3. NULL
    4. };
    5. int container_main(void* arg) {
    6. printf(“Container [%5d] – inside the container!\n”, getpid());
    7. sethostname(“container_dawn”, 15);
    8. if (mount(“proc”, “rootfs/proc”, “proc”, 0, NULL) != 0) {
    9. perror(“proc”);
    10. }
    11. if (mount(“sysfs”, “rootfs/sys”, “sysfs”, 0, NULL)!=0) {
    12. perror(“sys”);
    13. }
    14. if (mount(“none”, “rootfs/tmp”, “tmpfs”, 0, NULL)!=0) {
    15. perror(“tmp”);
    16. }
    17. if (mount(“udev”, “rootfs/dev”, “devtmpfs”, 0, NULL)!=0) {
    18. perror(“dev”);
    19. }
    20. if (mount(“devpts”, “rootfs/dev/pts”, “devpts”, 0, NULL)!=0) {
    21. perror(“dev/pts”);
    22. }
    23. if (mount(“shm”, “rootfs/dev/shm”, “tmpfs”, 0, NULL)!=0) {
    24. perror(“dev/shm”);
    25. }
    26. if (mount(“tmpfs”, “rootfs/run”, “tmpfs”, 0, NULL)!=0) {
    27. perror(“run”);
    28. }
    29. if ( chdir(“./rootfs”) || chroot(“./”) != 0 ){
    30. perror(“chdir/chroot”);
    31. }
    32. // 改变根目录之后,那么 /bin/bash 是从改变之后的根目录中搜索了
    33. execv(container_args[0], container_args);
    34. perror(“exec”);
    35. printf(“Something’s wrong!\n”);
    36. return 1;
    37. }
    38. int main() {
    39. printf(“Parent [%5d] – start a container!\n”, getpid());
    40. int container_id = clone(container_main, container_stack + STACK_SIZE,
    41. CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
    42. waitpid(container_id, NULL, 0);
    43. printf(“Parent – container stopped!\n”);
    44. return 0;
    45. }
  • *后,查看实现效果如下图所示。

    %title插图%num

实际上,Mount Namespace 是基于 chroot 的不断改良才被发明出来的,chroot 可以算是 Linux 中*个 Namespace。那么上面被挂载在容器根目录上、用来为容器镜像提供隔离后执行环境的文件系统,就是所谓的容器镜像,也被叫做 rootfs(根文件系统)。需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。

%title插图%num

User Namespace

容器内部看到的 UID 和 GID 和外部是不同的了,比如容器内部针对 dawn 这个用户显示的是 0,但是实际上这个用户在主机上应该是 1000。要实现这样的效果,需要把容器内部的 UID 和主机的 UID 进行映射,需要修改的文件是 /proc/<pid>/uid_map 和 /proc/<pid>/gid_map,这两个文件的格式是

  1. ID-INSIDE-NS ID-OUTSIDE-NS LENGTH
  • ID-INSIDE-NS :表示在容器内部显示的 UID 或 GID
  • ID-OUTSIDE-NS:表示容器外映射的真实的 UID 和 GID
  • LENGTH:表示映射的范围,一般为 1,表示一一对应

比如,下面就是将真实的 uid=1000 的映射为容器内的 uid =0:

  1. $ cat /proc/8353/uid_map
  2. 0 1000 1

再比如,下面则表示把 namesapce 内部从 0 开始的 uid 映射到外部从 0 开始的 uid,其*大范围是无符号 32 位整型(下面这条命令是在主机环境中输入的)。

  1. $ cat /proc/$$/uid_map
  2. 0 0 4294967295

默认情况,设置了 CLONE_NEWUSER 参数但是没有修改上述两个文件的话,容器中默认情况下显示为 65534,这是因为容器找不到真正的 UID,所以就设置了*大的 UID。如下面的代码所示:

  1. #define _GNU_SOURCE
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <sys/types.h>
  5. #include <sys/wait.h>
  6. #include <sys/mount.h>
  7. #include <sys/capability.h>
  8. #include <stdio.h>
  9. #include <sched.h>
  10. #include <signal.h>
  11. #include <unistd.h>
  12. #define STACK_SIZE (1024 * 1024)
  13. static char container_stack[STACK_SIZE];
  14. char* const container_args[] = {
  15. “/bin/bash”,
  16. NULL
  17. };
  18. int container_main(void* arg) {
  19. printf(“Container [%5d] – inside the container!\n”, getpid());
  20. printf(“Container: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n”,
  21. (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
  22. printf(“Container [%5d] – setup hostname!\n”, getpid());
  23. //set hostname
  24. sethostname(“container”,10);
  25. execv(container_args[0], container_args);
  26. printf(“Something’s wrong!\n”);
  27. return 1;
  28. }
  29. int main() {
  30. const int gid=getgid(), uid=getuid();
  31. printf(“Parent: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n”,
  32. (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
  33. printf(“Parent [%5d] – start a container!\n”, getpid());
  34. int container_pid = clone(container_main, container_stack+STACK_SIZE,
  35. CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWUSER | SIGCHLD, NULL);
  36. printf(“Parent [%5d] – Container [%5d]!\n”, getpid(), container_pid);
  37. printf(“Parent [%5d] – user/group mapping done!\n”, getpid());
  38. waitpid(container_pid, NULL, 0);
  39. printf(“Parent – container stopped!\n”);
  40. return 0;
  41. }

当我以 dawn 这个用户执行的该程序的时候,那么会显示如下图所示的效果。使用 root 用户的时候是同样的:

%title插图%num %title插图%num

接下去,我们要开始来实现映射的效果了,也就是让 dawn 这个用户在容器中显示为 0。代码是几乎完全拿耗子叔的博客上的,链接可见文末:

  1. int pipefd[2];
  2. void set_map(char* file, int inside_id, int outside_id, int len) {
  3. FILE* mapfd = fopen(file, “w”);
  4. if (NULL == mapfd) {
  5. perror(“open file error”);
  6. return;
  7. }
  8. fprintf(mapfd, “%d %d %d”, inside_id, outside_id, len);
  9. fclose(mapfd);
  10. }
  11. void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
  12. char file[256];
  13. sprintf(file, “/proc/%d/uid_map”, pid);
  14. set_map(file, inside_id, outside_id, len);
  15. }
  16. int container_main(void* arg) {
  17. printf(“Container [%5d] – inside the container!\n”, getpid());
  18. printf(“Container: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n”,
  19. (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
  20. /* 等待父进程通知后再往下执行(进程间的同步) */
  21. char ch;
  22. close(pipefd[1]);
  23. read(pipefd[0], &ch, 1);
  24. printf(“Container [%5d] – setup hostname!\n”, getpid());
  25. //set hostname
  26. sethostname(“container”,10);
  27. //remount “/proc” to make sure the “top” and “ps” show container’s information
  28. mount(“proc”, “/proc”, “proc”, 0, NULL);
  29. execv(container_args[0], container_args);
  30. printf(“Something’s wrong!\n”);
  31. return 1;
  32. }
  33. int main() {
  34. const int gid=getgid(), uid=getuid();
  35. printf(“Parent: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n”,
  36. (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
  37. pipe(pipefd);
  38. printf(“Parent [%5d] – start a container!\n”, getpid());
  39. int container_pid = clone(container_main, container_stack+STACK_SIZE,
  40. CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
  41. printf(“Parent [%5d] – Container [%5d]!\n”, getpid(), container_pid);
  42. //To map the uid/gid,
  43. // we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
  44. set_uid_map(container_pid, 0, uid, 1);
  45. printf(“Parent [%5d] – user/group mapping done!\n”, getpid());
  46. /* 通知子进程 */
  47. close(pipefd[1]);
  48. waitpid(container_pid, NULL, 0);
  49. printf(“Parent – container stopped!\n”);
  50. return 0;
  51. }

实现的*终效果如图所示,可以看到在容器内部将 dawn 这个用户 UID 显示为了 0(root),但其实这个容器中的 /bin/bash 进程还是以一个普通用户,也就是 dawn 来运行的,只是显示出来的 UID 是 0,所以当查看 /root 目录的时候还是没有权限。

%title插图%num

User Namespace 是以普通用户运行的,但是别的 Namespace 需要 root 权限,那么当使用多个 Namespace 该怎么办呢?我们可以先用一般用户创建 User Namespace,然后把这个一般用户映射成 root,那么在容器内用 root 来创建其他的 Namespace。

%title插图%num

Network Namespace

隔离容器中的网络,每个容器都有自己的虚拟网络接口和 IP 地址。在 Linux 中,可以使用 ip 命令创建 Network Namespace(Docker 的源码中,它没有使用 ip 命令,而是自己实现了 ip 命令内的一些功能)。

下面就使用 ip 命令来讲解一下 Network Namespace 的构建,以 bridge 网络为例。bridge 网络的拓扑图一般如下图所示,其中 br0 是 Linux 网桥。

%title插图%num

在使用 Docker 的时候,如果启动一个 Docker 容器,并使用 ip link show 查看当前宿主机上的网络情况,那么你会看到有一个 docker0 还有一个 veth****  的虚拟网卡,这个 veth 的虚拟网卡就是上图中 veth,而 docker0 就相当于上图中的 br0。

那么,我们可以使用下面这些命令即可创建跟 docker 类似的效果(参考自耗子叔的博客,链接见文末参考,结合上图加了一些文字)。

  1. ## 1. 首先,我们先增加一个网桥 lxcbr0,模仿 docker0
  2. brctl addbr lxcbr0
  3. brctl stp lxcbr0 off
  4. ifconfig lxcbr0 192.168.10.1/24 up #为网桥设置IP地址
  5. ## 2. 接下来,我们要创建一个 network namespace ,命名为 ns1
  6. # 增加一个 namesapce 命令为 ns1 (使用 ip netns add 命令)
  7. ip netns add ns1
  8. # 激活 namespace 中的 loopback,即127.0.0.1(使用 ip netns exec ns1 相当于进入了 ns1 这个 namespace,那么 ip link set dev lo up 相当于在 ns1 中执行的)
  9. ip netns exec ns1 ip link set dev lo up
  10. ## 3. 然后,我们需要增加一对虚拟网卡
  11. # 增加一对虚拟网卡,注意其中的 veth 类型。这里有两个虚拟网卡:veth-ns1 和 lxcbr0.1,veth-ns1 网卡是要被安到容器中的,而 lxcbr0.1 则是要被安到网桥 lxcbr0 中的,也就是上图中的 veth。
  12. ip link add veth-ns1 type veth peer name lxcbr0.1
  13. # 把 veth-ns1 按到 namespace ns1 中,这样容器中就会有一个新的网卡了
  14. ip link set veth-ns1 netns ns1
  15. # 把容器里的 veth-ns1 改名为 eth0 (容器外会冲突,容器内就不会了)
  16. ip netns exec ns1 ip link set dev veth-ns1 name eth0
  17. # 为容器中的网卡分配一个 IP 地址,并激活它
  18. ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up
  19. # 上面我们把 veth-ns1 这个网卡按到了容器中,然后我们要把 lxcbr0.1 添加上网桥上
  20. brctl addif lxcbr0 lxcbr0.1
  21. # 为容器增加一个路由规则,让容器可以访问外面的网络
  22. ip netns exec ns1 ip route add default via 192.168.10.1
  23. ## 4. 为这个 namespace 设置 resolv.conf,这样,容器内就可以访问域名了
  24. echo “nameserver 8.8.8.8” > conf/resolv.conf

上面基本上就相当于 docker 网络的原理,只不过:

  • Docker 不使用 ip 命令而是,自己实现了 ip 命令内的一些功能。
  • Docker 的 resolv.conf 没有使用这样的方式,而是将其写到指定的 resolv.conf 中,之后在启动容器的时候将其和 hostname、host 一起以只读的方式加载到容器的文件系统中。
  • docker 使用进程的 PID 来做 network namespace 的名称。

同理,我们还可以使用如下的方式为正在运行的 docker 容器增加一个新的网卡

  1. ip link add peerA type veth peer name peerB
  2. brctl addif docker0 peerA
  3. ip link set peerA up
  4. ip link set peerB netns ${container-pid}
  5. ip netns exec ${container-pid} ip link set dev peerB name eth1
  6. ip netns exec ${container-pid} ip link set eth1 up
  7. ip netns exec ${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1

%title插图%num

Namespace 情况查看

Cgroup 的操作接口是文件系统,位于 /sys/fs/cgroup 中。假如想查看 namespace 的情况同样可以查看文件系统,namespace 主要查看 /proc/<pid>/ns 目录。

我们以上面的 [PID Namespace 程序](#PID Namespace) 为例,当这个程序运行起来之后,我们可以看到其 PID 为 11702。

%title插图%num

之后,我们保持这个子进程运行,然后打开另一个 shell,查看这个程序创建的子进程的 PID,也就是容器中运行的进程在主机中的 PID。

*后,我们分别查看 /proc/11702/ns 和 /proc/11703/ns 这两个目录的情况,也就是查看这两个进程的 namespace 情况。可以看到其中 cgroup、ipc、mnt、net、user 都是同一个 ID,而 pid、uts 是不同的 ID。如果两个进程的 namespace 编号相同,那么表示这两个进程位于同一个 namespace 中,否则位于不同 namespace 中。

%title插图%num

如果可以查看 ns 的情况之外,这些文件一旦被打开,只要 fd 被占用着,即使 namespace 中所有进程都已经结束了,那么创建的 namespace 也会一直存在。比如可以使用 mount --bind /proc/11703/ns/uts ~/uts,让 11703 这个进程的 UTS Namespace 一直存在。

%title插图%num

总结

Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的”视图“已经被操作系统做了限制,只能”看到“某些指定的内容,这仅仅对应用进程产影响但是对宿主机来说,这些被隔离了的进程,其实还是进程,跟宿主机上其他进程并无太大区别,都由宿主机统一管理。只不过这些被隔离的进程拥有额外设置过的 Namespace 参数。那么 Docker 项目在这里扮演的,更多是旁路式的辅助和管理工作。如下左图所示

%title插图%num

因此,相比虚拟机的方式,容器会更受欢迎。这是假如使用虚拟机的方式作为应用沙盒,那么必须要由 Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且里面必须要运行一个完整的 Guest OS 才能执行用户的应用进程。这样就导致了采用虚拟机的方式之后,不可避免地带来额外的资源消耗和占用。根据实验,一个运行着 CentOS 的 KVM 虚拟机启动后,在不做优化的情况下,虚拟机就需要占用 100-200 MB 内存。此外,用户应用运行在虚拟机中,它对宿主机操作系统的调用就不可避免地要经过虚拟机软件的拦截和处理,这本身就是一层消耗,尤其对资源、网络和磁盘 IO 的损耗非常大。

而假如使用容器的方式,容器化之后应用本质还是宿主机上的一个进程,这也就意味着因为虚拟机化带来的性能损耗是不存在的;而另一方面使用 Namespace 作为隔离手段的容器并不需要单独的 Guest OS,这就使得容器额外的资源占用几乎可以忽略不计。

总得来说,“敏捷”和“高性能”是容器相对于虚拟机*大的优势,也就是容器能在 PaaS 这种更加细粒度的资源管理平台上大行其道的重要原因。

但是!基于 Linux Namespace 的隔离机制相比于虚拟化技术也有很多不足之处,其中*主要的问题就是隔离不彻底。

  • 首先,容器只是运行在宿主机上的一种特殊进程,那么容器之间使用的还是同一个宿主机上的操作系统。尽管可以在容器里面通过 mount namesapce 单独挂载其他不同版本的操作系统文件,比如 centos、ubuntu,但是这并不能改变共享宿主机内核的事实。这就意味着你要在 windows 上运行 Linux 容器,或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器都是行不通的。

    而拥有虚拟机技术和独立 Guest OS 的虚拟机就要方便多了。

  • 其次,在 Linux 内核中,有很多资源和对象都是不能被 namespace 化的,比如时间。假如你的容器中的程序使用 settimeofday(2) 系统调用修改了时间,整个宿主机的时间都会被随之修改。

    相比虚拟机里面可以随意折腾的自由度,在容器里部署应用的时候,“什么能做,什么不能做” 是用户必须考虑的一个问题。之外,容器给应用暴露出来的攻击面是相当大的,应用“越狱”的难度也比虚拟机低很多。虽然,实践中可以使用 Seccomp 等技术对容器内部发起的所有系统调用进行过滤和甄别来进行安全加固,但这种方式因为多了一层对系统调用的过滤,也会对容器的性能产生影响。因此,在生产环境中没有人敢把运行在物理机上的 Linux 容器直接暴露到公网上。

另外,容器是一个“单进程”模型。容器的本质是一个进程,用户的应用进程实际上就是容器里 PID=1 的进程,而这个进程也是后续创建的所有进程的父进程。这也就意味着,在一个容器中,你没办法同时运行两个不同的应用,除非能事先找到一个公共的 PID=1 的程序来充当两者的父进程,比如使用 systemd 或者 supervisord。容器的设计更多是希望容器和应用同生命周期的,而不是容器还在运行,而里面的应用早已经挂了。

Docker私有镜像仓库是什么?

Docker镜像仓库概述

镜像仓库作为Docker技术的核心组件之一,其主要作用就是负责镜像内容的存储和分发。Docker镜像仓库从使用范围来说分为“公有镜像仓库”和“私有镜像仓库”,公有镜像仓库是可以被任何人使用的,例如Docker公司维护的在线存储库Docker Hub以及部分云服务厂商(如阿里云)提供的在线Docker镜像库等,都属于公有镜像仓库的范畴。

而私有镜像仓库则是指部署在公司或组织内部,用于自身应用Docker镜像存储、分发的镜像仓库。在构建公司内部使用的自动化发布系统的过程中,从安全的角度出发,应用的打包镜像一般情况下只会被存储在私有镜像仓库中,CI/CD流程的衔接点也是通过向私有镜像仓库上传镜像和拉取镜像的操作来完成的。

%title插图%num

在现阶段主流的企业级私有镜像仓库构建方案中,比较流行的是:开源的企业级Docker镜像仓库——Harbor、以及商业镜像仓库——JFrog Artifactory

这两种Docker镜像仓库各自都有一定的市场,就作者所工作过的公司来说使用Harbor和JFrog Artifactory作为私有镜像仓库的都有,但就成熟度和功能性完整性来说JFrog Artifactory作为商业级解决方案会更具优势,所以目前国内有钱的互联网公司选择JFrog Artifactory作为企业级私有仓库的比较多,本文的主要内容是演示如何通过Docker的方式来快速部署JFrog Artifactory并将其作为Devops自动发布系统的私有镜像仓库。

%title插图%num

JFrog Artifactory镜像仓库部署

根据官方介绍JFrogArtifactory是目前全球唯一一个支持所有开发语言,任意维度的元数据检索、跨语言正反向解析,并拥有深度递归、支持多活异地灾备的企业级、高可用二进制制品管理仓库。这里的二进制制品是指构建过程的输出物,包括软件包、测试报告,应用配置文件等可在服务器上直接运行或可查看的二进制软件制品。

JFrog Artifactory支持多种仓库类型,除了Docker镜像仓库外还支持Maven、Npm等其他类型的仓库。在正式企业级环境中镜像仓库的部署要考虑高可用、扩展性等要求,关于这方面的部署方式可以参考其他官方文档,本环节为了方便演示将采用Docker的方式进行部署,具体步骤如下:

1)、获取*新JFrog Artifactory社区版Docker镜像,命令如下:

$ docker pulldocker.bintray.io/jfrog/artifactory-jcr:latest

由于网络原因上述下载过程可能会比较慢,有条件的读者可以借助科学上网方式进行镜像获取。完成后可通过命令查看具体的镜像信息,如下:

$ docker images

2)、创建数据卷。考虑到镜像仓库作为持久化存储服务,这里为其创建单独的数据卷,命令如下:

  1. #创建数据目录
  2. $ mkdir -p ~/docker/volume/artifactory
  3. #切换到上述目录后执行数据卷创建命令
  4. $ docker volume create data_artifactory

3)、运行Docker容器,命令如下:

$ docker run --name jfrog-artifactory -d-v data_artifactory:/var/opt/jfrog/artifactory -p 8081:8081 -p 8082:8082docker.bintray.io/jfrog/artifactory-jcr:latest

完成上述操作后,如果容器运行成功,此时浏览器输入访问地址:http://127.0.0.1:8082,将会显示如图所示界面:

%title插图%num

输入初始密码(admin/password)后,会继续跳转到管理员密码重置界面,如图所示:

%title插图%num

依据步骤设置好管理员密码。之后会继续要求设置如订阅邮箱、访问网站域名、代理配置等信息,这里可以暂时忽略直接跳到仓库创建的页面,如图所示:

%title插图%num

从上图可以看到JFrogArtifactory支持多种类型的仓库,例如Maven私有仓库也可以通过JFrog Artifactory来进行配置,由于是社区版所以很多其他类型的仓库并未免费开放!这里我们只选择创建Docker镜像仓库,创建后效果如图所示:

%title插图%num

如上图所示,默认创建了一个RepositoryKey为docker-local的本地仓库。在实际工作场景中为了便于Docker镜像的管理,可以分别为同一代码空间的项目创建单独的镜像仓库,例如我们本书中所有的Spring Cloud实战项目创建一个单独镜像仓库,点击右上角“New LocalRepository”,如图所示:

%title插图%num

到这里JFrog Repository容器镜像仓库的基本部署工作就完成了,后面自动化发布系统中的CI/CD流程将以此为基础实现Docker镜像的存储、分发!关注我后续的内容将向你介绍如何构建一套完整的CI/CD自动发布系统!

Kubernetes 诞生七年,凭什么成为主流?

引言

作为一款开源的容器编排引擎,始于2014年的Kubernetes一经推出就受到了开发者的喜爱,在此之前,从来没有人想过能有一个同时被所有云供应商支持的分布式应用。在Kubernetes里,用户可以轻松地在虚拟机及硬件上运行程序,让开发者彻底摆脱过去以主机为中心的环境。Kubernetes在市场中并不是唯一的容器管理平台,但它支持用户只需要编写一次就能在所有类型的云供应商及私有云上运行,开发人员可以快速部署、随时拓展应用程序,同时降低了硬件的使用量。

经过了七年的发展历程,Kubernetes成为继Linux、Docker后*受欢迎的第三大平台的同时,也俨然已经成为云计算领域的主流应用。

近日,CSDN采访了VMware应用现代化业务部门研发副总裁、Kubernetes创始人Craig McLuckie,VMware首席工程师、Kubernetes创始人Joe Beda,VMware中国研发技术总监、CNCF Harbor开源项目创建人张海宁,邀请他们分享了Kubernetes的发展动态以及开源项目的成功秘诀。

1

容器与虚拟机的结合是必然趋势?

Kubernetes诞生七年,你认为它成功的秘诀是什么?

Joe Beda:Kubernetes在诞生之初我们就知道它在将来会有怎样的发展潜力,应该向什么方向发展。宛如儿时玩的拼图游戏,我们不仅有完成这个拼图所需要的所有部分,而且有完整的图纸。Kubernetes和其他项目相比,*大的不同之处在于:它是在正确的时机推出正确的创新。

更重要的是,我们在创建了Kubernetes之后吸引了更多人加入其中,把Kubernetes做成了一个社区,Kubernetes今天的成功正是靠后期集体的力量携手努力做到的,正如“独行快,众行远”这句谚语一般,这才是Kubernetes成功的关键。

随着云计算技术的应用与发展,未来容器会颠覆虚拟机吗?

Joe Beda:容器和虚拟机分别满足了不同的需求,但如果把这两个技术结合在一起,通常能够带来不错的结果。

虚拟机从一开始部署的时候就能实现很高水平的隔离,而且每一个虚拟机都有自己的内核,不会出现内核共用的情况。从安全的角度来看,界定清晰的安全边界能够让用户在使用的过程中更放心。

容器与hypervisor相比,它的优势在于启动和关闭都非常快速方便,和虚拟机相比,容器是一种更加轻量级的技术,多台容器可以共享同一个内核。另外,它的一个显著特点在于容器镜像方面,我们可以把同一个容器的镜像运行到笔记本电脑或者Kubernetes集群之上,而同一个容器的镜像在不同环境当中它的工作表现是一模一样的,这样当开发者跨越不同的环境进行开发就带来了使用上的便捷。

而且我们现在也看到越来越多的用户把容器和虚拟机放在一起来运行,就像我们此前讲到的这两个技术之间具有很强的互补性,这样一种部署能够让用户把两个技术的优势都发挥出来,比方说更高的开发效率、可移植性以及安全隔离等等。

%title插图%num

VMware首席工程师

Kubernetes创始人Joe Beda

VMware有没有计划推出容器本地的虚拟化?

Joe Beda:答案是肯定的,但我们是用不同的方法来实现的。比如说我们在vSphere 7当中内置了VMware本地的PODs,也就是说通过hypervisor来支持Kubernetes Pods。另外一个技术路径就是在Kubernetes的平台之上来托管虚拟机,长期以来VMware做了大量的投入和创新来不断完善hypervisor。

2

Kubernetes未来的发展方向

传统应用是通过购买成熟解决方案的方式获得的,我们选择现代应用的过程中应该直接选择购买还是自建应用程序呢?

Joe Beda:对于任何一个企业来说,无论是选择购买还是自建应用程序都要基于具体的业务需求以及市场上的现有产品来决定。今天我们看到,有越来越多针对Kubernetes的现成软件解决方案,打造现代化的应用程序不光是一个技术上的转型,也是一种文化上的转型。开发团队和运维团队之间一定要养成互动和反馈的良好习惯,这样才能帮助企业不断推出和构建可扩展性更好和可靠性更高的现代化应用。

Kubernetes有没有考虑过取代vSphere?未来发展方向在哪里?

Joe Beda:现在说Kubernetes替代vSphere肯定是为时过早的,我们认为Kubernetes和vSphere是互补的技术,而不是互相替代的技术。*初在创建Kubernetes的时候,考虑的就是将来要让Kubernetes运行到某种底层的基础架构之上,比如Google Compute Engine。所以今天我们看到Kubernetes无论是在Google Compute Engine还是在Amazon Elastic Compute Cloud (EC2),亦或是在VMware的vSphere上,都能够运行。

Craig McLuckie:事实上Kubernetes已经成为了一种新型的基础架构及服务IaaS的平台,这样就为用户带来了很多的机会,能够对他们任意环境当中的运维和开发工作进行标准化。而且对于Kubernetes来说,我们不仅是要把Kubernetes作为一个“终点”,也可以在构建任何一个目标系统的过程当中利用Kubernetes产生更多的效益。

%title插图%num

VMware应用现代化业务部门研发副总裁

Kubernetes创始人Craig McLuckie

3

如果构建一款成功的开源项目?

VMware Habor与Kubernetes一样,都是开源成功的典范,为什么会选择开源? 

张海宁:现在开源其实是一种主流的社区协作模式,当时将项目开源其实有两个目的。一是希望能有更多的用户来使用这项技术,对此进行反馈的同时方便我们检验市场的成熟度,从而对项目不断地优化。如果是闭源的话,其实很难迅速地获得大批量的用户。第二点,做开源是不是就意味着没有商业化的潜力?答案也不一定。开源以后,其实还是可以从中获取很多的商业价值的。Harbor本身是免费的,但是产品是要有企业去支持的,这二者之间并不矛盾,反而是一个互相配合的事情。

%title插图%num

VMware中国技术研发技术总监

CNCF Harbor开源项目创建人张海宁

Habor作为首个原创于中国的CNCF毕业项目,有哪些经验可以分享?

张海宁:首先,Harbor是可信的云原生制品仓库,*早其实它叫做企业级的镜像管理仓,是由VMware中国研发中心原创的一个开源项目,与Kubernetes一样,Harbor今年也7岁了。

CNCF的开源项目其实分了很多个级别,不同的级别代表不同的用户认可度和成熟度。*开始Sandbox沙箱是入门级,中间有一个Incubation孵化级,*后是Graduation毕业级。每一个项目在不同的级别中逐渐走向成熟,毕业级的考核是相当严格的,只有认证、检查、审计等等各个环节都达到要求以后才能毕业。同时毕业也意味着得到了国际开源组织对它的认可,在后面可以投入到生产环节中。