<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title>魔术师的帽子</title>
        <link>https://blog.magichc7.com</link>
        <description>秦关的个人技术分享博客</description>
        <atom:link href="https://blog.magichc7.com/rss.html" rel="self" />
        <language>zh-cn</language>
        <lastBuildDate>Thu, 16 Apr 2026 00:00:07 GMT</lastBuildDate>
        <item>
            <title>Kubernetes Device Plugin 原理讲解</title>
            <link>https://blog.magichc7.com/post/Kubernetes-Device-Plugin.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-e05">简介</a></li>
<li><a href="#toc-96b">Device Plugin 的注册和使用流程</a><ul>
<li><a href="#toc-168">注册部分</a></li>
<li><a href="#toc-91f">分配部分</a></li>
</ul>
</li>
<li><a href="#toc-d0c">Device Plugin 的局限性</a></li>
</ul>
</div><p>很简单的 Kubernetes Device Plugin 原理讲解</p>
<!--more-->

<h1><a id="toc-e05" class="anchor" href="#toc-e05"></a>简介</h1>
<p>Kubernetes Device Plugin 是官方提供的一种扩展系统可用设备类型的方法。通过 device plugin，可以将一种设备类型“接入” Kubernetes，允许调度器根据需求为容器分配这些设备，并由 kubelet 和 CRI 执行实际的挂载。该特性在 v1.10 成为 beta，目前尚在完善。Device Plugin 体系仅包含对文件的操作，比如将 /dev 目录下的指定设备或者一些指定文件挂载到容器的指定目录下，并不包含驱动配置等操作，因此容器需要在镜像中自行适配相关设备的驱动。</p>
<blockquote>
<p>设备插件<br>FEATURE STATE: Kubernetes v1.10 [beta]<br>Kubernetes 提供了一个 设备插件框架，你可以用它来将系统硬件资源发布到 Kubelet。<br>供应商可以实现设备插件，由你手动部署或作为 DaemonSet 来部署，而不必定制 Kubernetes 本身的代码。目标设备包括 GPU、高性能 NIC、FPGA、 InfiniBand 适配器以及其他类似的、可能需要特定于供应商的初始化和设置的计算资源。  </p>
</blockquote>
<h1><a id="toc-96b" class="anchor" href="#toc-96b"></a>Device Plugin 的注册和使用流程</h1>
<p>以Nvidia Device Plugin 为例
<img src="https://cdn.magichc7.com/static/upload/20220119/upload_f28715e8273be0fb542c43b1c4b1dfd0.png" alt="Device Plugin 工作流程.jpeg"></p>
<h2><a id="toc-168" class="anchor" href="#toc-168"></a>注册部分</h2>
<ol>
<li>首先，Device Plugin 通过 Kubelet 提供的 RPC 接口 Register 进行注册。</li>
</ol>
<pre><code class="hljs lang-go"><span class="hljs-comment">// Register registers the device plugin for the given resourceName with Kubelet.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(m *NvidiaDevicePlugin)</span> <span class="hljs-title">Register</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    conn, err := m.dial(pluginapi.KubeletSocket, <span class="hljs-number">5</span>*time.Second)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    <span class="hljs-keyword">defer</span> conn.Close()

    client := pluginapi.NewRegistrationClient(conn)
    reqt := &amp;pluginapi.RegisterRequest{
        Version:      pluginapi.Version,
        Endpoint:     path.Base(m.socket),
        ResourceName: m.resourceName,
        Options: &amp;pluginapi.DevicePluginOptions{
            GetPreferredAllocationAvailable: (m.allocatePolicy != <span class="hljs-literal">nil</span>),
        },
    }

    _, err = client.Register(context.Background(), reqt)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<p>在注册时，Device Plugin 需要提供的参数包括</p>
<ul>
<li>自身的版本号</li>
<li>自身提供的 RPC 服务监听的 Unix Socket 的地址。这个 Socket 必须放在  /var/lib/kubelet/device_plugins 目录下，Kubelet 重启时会清空这个目录下的 .sock 文件，因此 Device Plugin 需要监控文件的变化，当 Kubelet 重启的时候，重启 RPC 服务重新注册</li>
<li>自身托管的资源的名称。比如 nvidia.com/gpu。如果想提供多种可用设备的话，需要为每种资源分别启动一个 RPC Server 去注册。</li>
<li>注册的选项，主要包括两个 bool 型变量<ul>
<li>PreStartRequired: 容器启动前是否必须调用 PreStartContainer 方法</li>
<li>GetPreferredAllocationAvailable: 是否提供优选方法 GetPreferredAllocation，若不提供，则由调度器直接决定为容器分配哪些设备
有一点需要注意，
插件并非必须为 GetPreferredAllocation() 或 PreStartContainer() 提供有用 的实现逻辑，调用 GetDevicePluginOptions() 时所返回的 DevicePluginOptions 消息中应该设置这些调用是否可用。kubelet 在真正调用这些函数之前，总会调用 GetDevicePluginOptions() 来查看是否存在这些可选的函数。
此外，Device Plugin 需要感知自己的 .sock 有没有被删除，以判断 Kubelet 有没有重启。</li>
</ul>
</li>
</ul>
<ol start="2">
<li>Kubelet 根据注册信息，向 Device Plugin 发起 List-Watch 请求，获取可用设备的状态。</li>
</ol>
<pre><code class="hljs lang-go"><span class="hljs-comment">// ListAndWatch lists devices and update that list according to the health status</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(m *NvidiaDevicePlugin)</span> <span class="hljs-title">ListAndWatch</span><span class="hljs-params">(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListAndWatchServer)</span> <span class="hljs-title">error</span></span> {
    s.Send(&amp;pluginapi.ListAndWatchResponse{Devices: m.apiDevices()})

    <span class="hljs-keyword">for</span> {
        <span class="hljs-keyword">select</span> {
        <span class="hljs-keyword">case</span> &lt;-m.stop:
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
        <span class="hljs-keyword">case</span> d := &lt;-m.health:
            <span class="hljs-comment">// <span class="hljs-doctag">FIXME:</span> there is no way to recover from the Unhealthy state.</span>
            d.Health = pluginapi.Unhealthy
            log.Printf(<span class="hljs-string">"'%s' device marked unhealthy: %s"</span>, m.resourceName, d.ID)
            s.Send(&amp;pluginapi.ListAndWatchResponse{Devices: m.apiDevices()})
        }
    }
}
</code></pre>
<p>这里，NVIDIA Device Plugin 会有一个定时检查设备健康程度的 Goroutine，如果发现设备不健康，就会通过 health 这个 Channel 传递不健康设备的指针，然后将这个设备设置成不健康。注意，NVIDIA Device Plugin 没有 Recover 逻辑，一个设备一旦被设置为不健康了，在 Device Plugin 重启之前，这个设备将一直处于不可用状态。
对于每个设备，Kubelet 只关心设备的 ID，健康状态，以及绑核（Numa核）逻辑。</p>
<pre><code class="hljs lang-go"><span class="hljs-comment">// E.g:</span>
<span class="hljs-comment">// struct Device {</span>
<span class="hljs-comment">//    ID: "GPU-fef8089b-4820-abfc-e83e-94318197576e",</span>
<span class="hljs-comment">//    Health: "Healthy",</span>
<span class="hljs-comment">//    Topology:</span>
<span class="hljs-comment">//      Node:</span>
<span class="hljs-comment">//        ID: 1</span>
<span class="hljs-comment">//}</span>
<span class="hljs-keyword">type</span> Device <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// A unique ID assigned by the device plugin used</span>
    <span class="hljs-comment">// to identify devices during the communication</span>
    <span class="hljs-comment">// Max length of this field is 63 characters</span>
    ID <span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`</span>
    <span class="hljs-comment">// Health of the device, can be healthy or unhealthy, see constants.go</span>
    Health <span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,2,opt,name=health,proto3" json:"health,omitempty"`</span>
    <span class="hljs-comment">// Topology for device</span>
    Topology             *TopologyInfo <span class="hljs-string">`protobuf:"bytes,3,opt,name=topology,proto3" json:"topology,omitempty"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{}      <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>         <span class="hljs-string">`json:"-"`</span>
}
</code></pre>
<ol start="3">
<li>Kubelet 通过 List-Watch 拿到可用设备列表后，会 Patch 更新 Node Allocatable 信息，并在本地文件 /var/lib/kubelet/device-plugins/kubelet_internal_checkpoint 中更新一份可用设备列表的缓存。</li>
<li>Scheduler 通过 List-Watch 获取 Node 可用的设备，并在调度时根据 Pod Spec 进行分配。</li>
</ol>
<h2><a id="toc-91f" class="anchor" href="#toc-91f"></a>分配部分</h2>
<ol>
<li>Scheduler 完成调度后，会将 Pod Bind 到节点上，此时 Kubelet 会通过 List-Watch 感知到这一事件，并开始调用 Device Plugin 来为 Pod 准备设备的挂载等操作。</li>
<li>如前所述，Kubelet 先调用 GetDevicePluginOptions 获取当前 Device Plugin 提供的能力，比如是否可以优选，是否可以在容器启动前执行一些逻辑。</li>
<li>如果配置了优选，那么 Kubelet 会调用 Device Plugin 提供的 GetPreferredAllocation 方法。发送一系列 ContainerPreferredAllocationRequest，每个请求包含可用的设备列表和必须包含的设备列表。由 Device Plugin 返回根据策略选出的设备列表。</li>
</ol>
<pre><code class="hljs lang-go"><span class="hljs-comment">// PreferredAllocationRequest is passed via a call to GetPreferredAllocation()</span>
<span class="hljs-comment">// at pod admission time. The device plugin should take the list of</span>
<span class="hljs-comment">// `available_deviceIDs` and calculate a preferred allocation of size</span>
<span class="hljs-comment">// 'allocation_size' from them, making sure to include the set of devices</span>
<span class="hljs-comment">// listed in 'must_include_deviceIDs'.</span>
<span class="hljs-keyword">type</span> PreferredAllocationRequest <span class="hljs-keyword">struct</span> {
    ContainerRequests    []*ContainerPreferredAllocationRequest <span class="hljs-string">`protobuf:"bytes,1,rep,name=container_requests,json=containerRequests,proto3" json:"container_requests,omitempty"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{}                               <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>                                  <span class="hljs-string">`json:"-"`</span>
}

<span class="hljs-keyword">type</span> ContainerPreferredAllocationRequest <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// List of available deviceIDs from which to choose a preferred allocation</span>
    AvailableDeviceIDs []<span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,1,rep,name=available_deviceIDs,json=availableDeviceIDs,proto3" json:"available_deviceIDs,omitempty"`</span>
    <span class="hljs-comment">// List of deviceIDs that must be included in the preferred allocation</span>
    MustIncludeDeviceIDs []<span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,2,rep,name=must_include_deviceIDs,json=mustIncludeDeviceIDs,proto3" json:"must_include_deviceIDs,omitempty"`</span>
    <span class="hljs-comment">// Number of devices to include in the preferred allocation</span>
    AllocationSize       <span class="hljs-keyword">int32</span>    <span class="hljs-string">`protobuf:"varint,3,opt,name=allocation_size,json=allocationSize,proto3" json:"allocation_size,omitempty"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{} <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>    <span class="hljs-string">`json:"-"`</span>
}

<span class="hljs-comment">// PreferredAllocationResponse returns a preferred allocation,</span>
<span class="hljs-comment">// resulting from a PreferredAllocationRequest.</span>
<span class="hljs-keyword">type</span> PreferredAllocationResponse <span class="hljs-keyword">struct</span> {
    ContainerResponses   []*ContainerPreferredAllocationResponse <span class="hljs-string">`protobuf:"bytes,1,rep,name=container_responses,json=containerResponses,proto3" json:"container_responses,omitempty"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{}                                <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>                                   <span class="hljs-string">`json:"-"`</span>
}

<span class="hljs-keyword">type</span> ContainerPreferredAllocationResponse <span class="hljs-keyword">struct</span> {
    DeviceIDs            []<span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,1,rep,name=deviceIDs,proto3" json:"deviceIDs,omitempty"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{} <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>    <span class="hljs-string">`json:"-"`</span>
}

<span class="hljs-comment">// GetPreferredAllocation returns the preferred allocation from the set of devices specified in the request</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(m *NvidiaDevicePlugin)</span> <span class="hljs-title">GetPreferredAllocation</span><span class="hljs-params">(ctx context.Context, r *pluginapi.PreferredAllocationRequest)</span> <span class="hljs-params">(*pluginapi.PreferredAllocationResponse, error)</span></span> {
    response := &amp;pluginapi.PreferredAllocationResponse{}
    <span class="hljs-keyword">for</span> _, req := <span class="hljs-keyword">range</span> r.ContainerRequests {
        available, err := gpuallocator.NewDevicesFrom(req.AvailableDeviceIDs)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"Unable to retrieve list of available devices: %v"</span>, err)
        }

        required, err := gpuallocator.NewDevicesFrom(req.MustIncludeDeviceIDs)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"Unable to retrieve list of required devices: %v"</span>, err)
        }

        allocated := m.allocatePolicy.Allocate(available, required, <span class="hljs-keyword">int</span>(req.AllocationSize))

        <span class="hljs-keyword">var</span> deviceIds []<span class="hljs-keyword">string</span>
        <span class="hljs-keyword">for</span> _, device := <span class="hljs-keyword">range</span> allocated {
            deviceIds = <span class="hljs-built_in">append</span>(deviceIds, device.UUID)
        }

        resp := &amp;pluginapi.ContainerPreferredAllocationResponse{
            DeviceIDs: deviceIds,
        }

        response.ContainerResponses = <span class="hljs-built_in">append</span>(response.ContainerResponses, resp)
    }
    <span class="hljs-keyword">return</span> response, <span class="hljs-literal">nil</span>
}
</code></pre>
<ol start="4">
<li>Allocate 执行实际的分配行为，要求 Device Plugin 根据分配请求，提供需要注入的环境变量、 Annotation 和挂载行为，这些均仅 CRI 可见，不会体现在 Pod Spec 上。  </li>
</ol>
<pre><code class="hljs lang-go"><span class="hljs-comment">// - Allocate is expected to be called during pod creation since allocation</span>
<span class="hljs-comment">//   failures for any container would result in pod startup failure.</span>
<span class="hljs-comment">// - Allocate allows kubelet to exposes additional artifacts in a pod's</span>
<span class="hljs-comment">//   environment as directed by the plugin.</span>
<span class="hljs-comment">// - Allocate allows Device Plugin to run device specific operations on</span>
<span class="hljs-comment">//   the Devices requested</span>
<span class="hljs-keyword">type</span> AllocateRequest <span class="hljs-keyword">struct</span> {
    ContainerRequests    []*ContainerAllocateRequest <span class="hljs-string">`protobuf:"bytes,1,rep,name=container_requests,json=containerRequests,proto3" json:"container_requests,omitempty"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{}                    <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>                       <span class="hljs-string">`json:"-"`</span>
}

<span class="hljs-keyword">type</span> ContainerAllocateRequest <span class="hljs-keyword">struct</span> {
    DevicesIDs           []<span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,1,rep,name=devicesIDs,proto3" json:"devicesIDs,omitempty"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{} <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>    <span class="hljs-string">`json:"-"`</span>
}

<span class="hljs-comment">// AllocateResponse includes the artifacts that needs to be injected into</span>
<span class="hljs-comment">// a container for accessing 'deviceIDs' that were mentioned as part of</span>
<span class="hljs-comment">// 'AllocateRequest'.</span>
<span class="hljs-comment">// Failure Handling:</span>
<span class="hljs-comment">// if Kubelet sends an allocation request for dev1 and dev2.</span>
<span class="hljs-comment">// Allocation on dev1 succeeds but allocation on dev2 fails.</span>
<span class="hljs-comment">// The Device plugin should send a ListAndWatch update and fail the</span>
<span class="hljs-comment">// Allocation request</span>
<span class="hljs-keyword">type</span> AllocateResponse <span class="hljs-keyword">struct</span> {
    ContainerResponses   []*ContainerAllocateResponse <span class="hljs-string">`protobuf:"bytes,1,rep,name=container_responses,json=containerResponses,proto3" json:"container_responses,omitempty"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{}                     <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>                        <span class="hljs-string">`json:"-"`</span>
}

<span class="hljs-keyword">type</span> ContainerAllocateResponse <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// List of environment variable to be set in the container to access one of more devices.</span>
    Envs <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,1,rep,name=envs,proto3" json:"envs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`</span>
    <span class="hljs-comment">// Mounts for the container.</span>
    Mounts []*Mount <span class="hljs-string">`protobuf:"bytes,2,rep,name=mounts,proto3" json:"mounts,omitempty"`</span>
    <span class="hljs-comment">// Devices for the container.</span>
    Devices []*DeviceSpec <span class="hljs-string">`protobuf:"bytes,3,rep,name=devices,proto3" json:"devices,omitempty"`</span>
    <span class="hljs-comment">// Container annotations to pass to the container runtime</span>
    Annotations          <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span> <span class="hljs-string">`protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`</span>
    XXX_NoUnkeyedLiteral <span class="hljs-keyword">struct</span>{}          <span class="hljs-string">`json:"-"`</span>
    XXX_sizecache        <span class="hljs-keyword">int32</span>             <span class="hljs-string">`json:"-"`</span>
}

<span class="hljs-comment">// Allocate which return list of devices.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(m *NvidiaDevicePlugin)</span> <span class="hljs-title">Allocate</span><span class="hljs-params">(ctx context.Context, reqs *pluginapi.AllocateRequest)</span> <span class="hljs-params">(*pluginapi.AllocateResponse, error)</span></span> {
    responses := pluginapi.AllocateResponse{}
    <span class="hljs-keyword">for</span> _, req := <span class="hljs-keyword">range</span> reqs.ContainerRequests {
        <span class="hljs-keyword">for</span> _, id := <span class="hljs-keyword">range</span> req.DevicesIDs {
            <span class="hljs-keyword">if</span> !m.deviceExists(id) {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"invalid allocation request for '%s': unknown device: %s"</span>, m.resourceName, id)
            }
        }

        response := pluginapi.ContainerAllocateResponse{}

        uuids := req.DevicesIDs
        deviceIDs := m.deviceIDsFromUUIDs(uuids)

        <span class="hljs-keyword">if</span> deviceListStrategyFlag == DeviceListStrategyEnvvar {
            response.Envs = m.apiEnvs(m.deviceListEnvvar, deviceIDs)
        }
        <span class="hljs-keyword">if</span> deviceListStrategyFlag == DeviceListStrategyVolumeMounts {
            response.Envs = m.apiEnvs(m.deviceListEnvvar, []<span class="hljs-keyword">string</span>{deviceListAsVolumeMountsContainerPathRoot})
            response.Mounts = m.apiMounts(deviceIDs)
        }
        <span class="hljs-keyword">if</span> passDeviceSpecsFlag {
            response.Devices = m.apiDeviceSpecs(nvidiaDriverRootFlag, uuids)
        }

        responses.ContainerResponses = <span class="hljs-built_in">append</span>(responses.ContainerResponses, &amp;response)
    }

    <span class="hljs-keyword">return</span> &amp;responses, <span class="hljs-literal">nil</span>
}
</code></pre>
<p>Device Plugin 所要做的就是构造 ContainerAllocateResponse 响应体，返回 Annotation，Env，Mounts。实际上，对于 NVIDIA Runtime，只需要在容器中配置 NVIDIA_VISIBLE_DEVICES 这个环境变量，在 CRI 中会自动根据该项去挂载对应的 GPU 设备以及相关的 toolkit，如果配置了 NVIDIA_VISIBLE_DEVICES=all，则挂载全部设备。不需要返回 Annotation 和 Mount 信息。<br>5. 执行启动前逻辑。如果 Device Plugin 在注册时或者 GetDevicePluginOptions 中声明了要求执行启动前逻辑，则 Kubelet 会在 CRI 配置好容器后，在启动前调用 PreStartContainer 逻辑，此时 Pod 依然处于 Pending 状态，直到调用完毕。</p>
<h1><a id="toc-d0c" class="anchor" href="#toc-d0c"></a>Device Plugin 的局限性</h1>
<ul>
<li>Device Plugin 只能根据分配的结果去做相应的操作，或者在单机层面做一些单机层面的优选行为，却不能介入节点的筛选过程。</li>
<li>设备的分配是依据 Index 来决定的，以单个设备为粒度进行分配，不支持按份数拆分。因此， Aliyun 的 GPU 共享插件为了支持按显存分配，将每一 G 显存都设置成了一个单独的设备。</li>
<li>所有的 RPC 接口都不提供 Pod 信息，无法感知每一块设备具体是被哪个 Pod 分走了。为此，Aliyun 的做法是同时侵入调度器，在调度时，为 Pod 打上 Annotation，然后在 Device Plugin 中，通过 Kubelet 获取当前节点上 Pending 的 Pod，根据 Annotation 的内容做对应。字节跳动内部为了解决这个问题对 Kubelet 本身做了 Hack，在调用传递的 Context 中，封入了 Pod 的 Name 和 Namespace 信息。该特性有社区在提交 PR，但是两年没合并。</li>
<li>Device Plugin 鼓励的工作模式是无状态的，希望开发者不要在 Device Plugin 中记录任何的状态信息。</li>
</ul>

            ]]></description>
            <pubDate>Wed, 19 Jan 2022 05:22:25 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/Kubernetes-Device-Plugin.html</guid>
        </item>
        <item>
            <title>《一周影评》：丹尼尔.克雷格的《007》</title>
            <link>https://blog.magichc7.com/post/moviereviews-3.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-b95">剧情点评</a><ul>
<li><a href="#toc-f2c">皇家赌场 （2006）</a></li>
<li><a href="#toc-a5d">量子危机（2008）</a></li>
<li><a href="#toc-d74">天幕杀机（2012）</a></li>
<li><a href="#toc-e3b">幽灵党（2015）</a></li>
<li><a href="#toc-a77">无暇赴死（2021）</a></li>
</ul>
</li>
<li><a href="#toc-6b3">不一样的詹姆斯.邦德</a></li>
<li><a href="#toc-25f">总结</a></li>
</ul>
</div><p>无暇赴死是一部烂片，但也是一场盛大的闭幕式
<img src="https://cdn.magichc7.com/static/upload/20220116/upload_42c48bf3f6108e72d0117653a4c9c80f.png" alt="image.png"></p>
<!--more-->

<h1><a id="toc-b95" class="anchor" href="#toc-b95"></a>剧情点评</h1>
<p>15年间，丹尼尔.克雷格从2006年的《皇家赌场》，到2021年的《无暇赴死》，一共参演了5部007电影。早在《无暇赴死》之前，就总有消息称丹尼尔可能不会主演下一部007。但是每一次，那个不守规矩的MI6特工又都会出现在荧幕上飞檐走壁。《无暇赴死》中，伴随着漫天的导弹雨，邦德死在了最后一次任务中，大家都知道，小强这一次，真的谢幕了。</p>
<h2><a id="toc-f2c" class="anchor" href="#toc-f2c"></a>皇家赌场  （2006）</h2>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_0ac46f279b0d20394fdf40c9e4e67ce1.png" alt="image.png"></p>
<p>五部电影里，我最喜欢的还是《皇家赌场》。这是反派和编剧最正常的一部，也是元素最齐全的一部：有武装到排气管的007专用捷豹，有纸醉金迷的奢华生活，有性感迷人的邦女郎 Vesper，有老谋深算的M夫人，最关键的，是有最年轻的丹尼尔。后来的几部电影里，要么是反派拉了或者剧情乱写（占绝大多数），要么是没有能留下印象的邦女郎。贯彻始终只有邦德和他的悲情色彩。他的朋友、领导、前辈、兄弟、情人死了个遍。从头到尾，他只保护下了 Swann 和自己的女儿。<br>在《皇家赌场》里，邦德遇见了那个影响了他一生的女人 Vesper 。本来 Vesper 也会像其他邦女郎一样成为另一个符号，但是后续的作品不断拔高了 Vesper 对邦德的影响。Vesper 带给邦德的幸福是短暂的，而她留给邦德的有关信任的诅咒，直到 Swann 才真正解开。  </p>
<h2><a id="toc-a5d" class="anchor" href="#toc-a5d"></a>量子危机（2008）</h2>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_df7bad42a6c7921a520646a05e81c6cd.png" alt="image.png"></p>
<p>最平庸的应该就是第二部《量子危机》。这一部其实很重要，因为它解释了Vesper与邦德之间最重要的问题：“Vesper到底有没有背叛邦德”。答案是没有，Vesper的男友本来就是个恐怖分子，她也只是又一个被骗的女孩而已。Vesper与《皇家赌场》里，死在沙滩的女配角没有任何区别——被一个特工利用，然后被抛弃。  除此之外，《量子危机》的任务就是引出量子组织。但是，因为这部电影很不巧，赶上了美国电影编剧大罢工事件，剧本是匆匆忙忙赶出来的，很多细节都没有敲定，都是现场设计。因此这部电影的质量相较于前一部就断崖式的下滑。</p>
<h2><a id="toc-d74" class="anchor" href="#toc-d74"></a>天幕杀机（2012）</h2>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_e1566b39282fb4a4fb48a61133c506ca.png" alt="image.png"></p>
<p>《天幕杀机》是新旧角色交替最多的一部，新角色Q博士和M先生出现，M夫人下线。M夫人代表着老派情报机构，苍老而仍有其坚持。  </p>
<blockquote>
<p>「虽然我们的力量已不如当初，已非昔日雄姿可以震天撼地，但我们仍有著英雄的情怀。虽然时间和命运使其衰老，但坚强的意志仍在，让我们去奋斗、去追寻、去发现，並且永不屈服。」   </p>
</blockquote>
<p>这一部也探讨了特工的意义。不再有利用价值的特工，转眼被组织抛弃。特工到底是零件，还是人。用一个比邦德更优秀的特工席尔瓦，来拷问邦德：你是不是下一个弃子。会不会有一天，你也在那个玻璃笼子里。  </p>
<blockquote>
<p>&quot;I made my own choices&quot;
<img src="https://cdn.magichc7.com/static/upload/20220116/upload_f791e7622a2459de32141733b63e55c2.png" alt="image.png">  </p>
</blockquote>
<p>这里我引用一下知乎上<a href="https://www.zhihu.com/question/20746667/answer/16042615">SydneyCarton的回答</a>：  </p>
<blockquote>
<p>17、冷战结束了，这块比战争更适合间谍片成长的土壤一下子贫瘠了。在白宫都已经被炸过无数遍的银幕上，邦德的敌人是谁？这个时代的银幕荧屏英雄不再是风度翩翩永远正义的占士邦，而是更加草根和内心矛盾的包智杰。
  18、所以席尔瓦出现了，他是邦德的复本，一个甚至曾经更出色但被无情抛弃的间谍，他就是《第一滴血》首部曲里的兰博，带着曾有的荣光和熊熊复仇之火向帝国反噬。<br>19、熊熊烈火，烧掉的不只是邦德的祖宅，更是过往的历史包袱，随着正义与仇恨之母的离去，邦德终于从过往中彻底走出了。系列的logo或许会保留，但只是作为身份的呈现。作为神话的邦德享年五十，作为人的邦德诞生了。<br>20、原有规则被打破了，没有规则就是新规则。007放下了身段，融入了间谍动作片的大趋势中。我不知这种无规则会维持多久，但这部skyfall注定要在邦德百年时被标上三个字：转折点。</p>
</blockquote>
<h2><a id="toc-e3b" class="anchor" href="#toc-e3b"></a>幽灵党（2015）</h2>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_7b562ade008bde1ed613112814da3c59.png" alt="image.png"></p>
<p>《幽灵党》加上《无暇赴死》在我看来，有点像《咒术回战》中的《起始雷同》篇。邦德遇上了他生命中第二个重要的女人——斯旺医生。只是这一次，他成功地拯救了她，也有了自己的孩子。《幽灵党》点出了007世界的最大反派幽灵党。不过，我觉得邦德的兄弟这个设定很多余，他与席尔瓦，勒西弗这些反派没有什么本质的区别，换成任何一个身份都不会对剧情有什么影响。除了可以解释一个地下世界的头子为什么没事非要跟一个高级打工仔过不去以外，没什么意义。<br>《幽灵党》里，与邦德的兄弟阋墙相比，更重要的故事是九眼计划。新的谍报方式要取代旧的谍报方式。电影里，邦德抓住了幽灵党的老大，M先生阻止了九眼计划。现实里，五眼联盟已经是众所周知的组织。现实里如果有与英国分享情报的机会，根本不需要英国政府的白手套去别的国家使坏来让别人加入，那些小国恨不得马上签字。用特工干湿活来收集情报的效率太低了，一个新冠搞不好直接就全隔离了，国境都进不了。基于数据来收集情报才是王道，滴滴属实是政治觉悟太低，大局观淡薄，当然，也可能滴滴内部也有“莫娘”这样的人物在，打着为公司好的名头偷偷使坏纳投名状，一个猜想，不一定对。  </p>
<h2><a id="toc-a77" class="anchor" href="#toc-a77"></a>无暇赴死（2021）</h2>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_12ce81e55526941658517528143883a1.png" alt="image.png"></p>
<p>《无暇赴死》这部电影的剧情和反派是很扯淡的。前面四部电影，从勒西弗，到怀特，到他兄弟恩内斯特.布鲁佛，其实都可以算是与幽灵党有关的人，量子组织也只不过是幽灵党的一个触手而已。但是，我看第五部最大的感觉就是：怎么又开始了？没完了？不让人退休了是吗？现在什么人都能当恐怖分子的头目了是吗？随手灭掉了幽灵党全员，然后就缩在一个岛上卖军火？我都搞不懂了，你小时候没把 Swann 直接杀了，你长大了又回来复仇有意思吗？</p>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_1e8bb0c3fc5ac2c32923545cb3c75bb5.png" alt="image.png"></p>
<p>这部电影也是我对邦德产生最深的同情的一部，“江湖儿郎江湖死”。邦德和他爱人身上都背负了太多的鲜血。就像“咒术师不存在无悔的死亡”一样，杀手也得不到善终。被邦德杀掉的那些人也曾跟邦德一样，出生入死，无所不能。然后被一个来复仇的疯子，或者被一个正义英雄杀死。他想要不孤独的平静，但是从前的一切没有放过他。再一次感觉自己被背叛之后，他孤独地度过了人生最后五年的平静时光。</p>
<blockquote>
<p>&quot;We have all the time in the world&quot;<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_4136c1d1a1b4a59e0d1742e1d49e508e.png" alt="image.png"><br>&quot;If we would have more time&quot;<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_a81828d56fc8439509ff2f29b96f3cfa.png" alt="image.png">  </p>
</blockquote>
<p>这部电影也是邦德最像一个普通人的一部。他苍老了，做任务不再是一个人就能摧毁一个王国了，开始需要队友的配合了，他有自己的牵挂了。很难想象，&quot;I do not regret a single moment in my life to let me to you（我不后悔生命中每一个把我带到你身边的时刻）&quot;这句话出自15年前的那个杀手口中，更遑论他会热泪盈眶地说出我也爱你。  </p>
<blockquote>
<p>&quot;I love you, too.&quot;<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_18bf6fa918744bd6cc9ea19335bb6624.png" alt="image.png">  </p>
</blockquote>
<p>好在，导演给邦德安排了一个足够壮烈的死法：被集束导弹轰炸。说起来，“在最后一场战斗里，被最后一颗子弹打死”实在未免太小气了一点，说不定还会诈尸。还得是死于地毯式轰炸中，人们才愿意相信，这个小强是真的死了。导演也在最后一刻，让女儿的嘟嘟陪在他的身边。  </p>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_42c48bf3f6108e72d0117653a4c9c80f.png" alt="image.png"></p>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_30731a1c2f91193ce725be807fb22590.png" alt="image.png"></p>
<p><img src="https://cdn.magichc7.com/static/upload/20220116/upload_710dd02d453d58548249a89cb7198716.png" alt="image.png"></p>
<h1><a id="toc-6b3" class="anchor" href="#toc-6b3"></a>不一样的詹姆斯.邦德</h1>
<p>很多影评中总结，这几部电影与之前的007系列以及其他的间谍片风格不同，007系列回归到詹姆斯.邦德这个“人”身上。丹尼尔的这个系列里，每个反派以及牺牲者都跟邦德或多或少的有关系。影片更集中的表现了007特工作为一个人的挣扎。他从来都不是冰冷的任务机器。他是一个冷酷的特工，但是他有自己的爱恨情仇。我觉得如果要说《无暇赴死》的好的话，那就是他完成了<strong>“007在死亡的那一刻也是一个有情感的人”</strong>这个命题。<br>《皇家赌场》里，M夫人询问邦德：“我希望你能保持理性，但我想那对你来说不是问题，对吧，邦德？”。事实上，五部电影里，我们知道邦德从来都不理性。他是一个非常感情用事的人。他为了Vesper放弃了特工的生活，又因为被背叛，回归了杀戮的世界。口口声声说自己已经忘记了Vesper，却总是魂牵梦萦。老友马修斯死前对邦德说：“forgive her, forgive yourself”，邦德在墓前烧掉了“Forgive Me”。为了实现自己心中的正义，一次次的违抗命令，拉着可怜的Q博士帮他打掩护。这也是《幽灵党》中，“00”计划被视为过时的原因，因为特工是人，人不可控。<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_9139160c44270f123fc1006a5f984e56.png" alt="image.png"><br>邦德善于伪装。他总是能通过克制情绪的表达，来伪装成一个理性的机器。《无暇赴死》里，在曾经被背叛过的地方，再一次被幽灵党包围时，坐在防弹车里，任何一个普通人都不可能不愤怒，不歇斯底里。而邦德静静地坐在驾驶座上，蓝色的眼睛里，全都是悲凉。最后一声轻叹，他选择相信眼前的这个人，但是不愿再与她有交集。邦德只在生命的最后一刻，没有压抑自己的情感。他有自己的牵挂，他是一个父亲。  </p>
<blockquote>
<p>&quot;She does have your eye.&quot; &quot;I Konw&quot;<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_c20c7d13a2350938f5b30117ea7882f2.png" alt="image.png"></p>
</blockquote>
<p>丹尼尔自己在《无暇赴死》的杀青现场说：“这是他这辈子最荣幸的事情”。可我觉得，也是丹尼尔成就了这5部电影。如果不是他扮演这一代007，后面几部电影的豆瓣评分应该会更低一点。我试着看过一些更老的007电影，我觉得太无趣了。那种007，与《Kingsman》里的科林叔更像，端着老派英国绅士的那种调调，风度翩翩，又有些油腔滑调。丹尼尔的007，与《速度与激情》系列里的匪帮风格更为接近。这一代的007总是把自己搞的狼狈不堪，浑身伤痕，但这才是一个真实的有血有肉的人，不是一个开了无敌挂的家伙（当然，丹尼尔开的挂也不少）。这样的一个007的形象，就需要丹尼尔这种 Tough Guy 来扮演。这五部007系列的剧情一直在及格线附近徘徊，但是演员的演技一直全程在线。如果没有这些优秀的演员，没有优秀的电影工业塑造的优秀的声光场面，007一定是烂片。<br>这一代的邦德的不同之处，也可以通过邦女郎体现。五部电影里，我觉得只有M、Vesper、Swann以及那位“训练了三周的新人”Armas算是真正的邦女郎，其他的那些充其量都是炮灰。（说起来我这周刚好还看了《银翼杀手2049》）  </p>
<blockquote>
<p>Mrs M<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_a4f90f3d1d34d168204581c9840adc43.png" alt="image.png"></p>
</blockquote>
<blockquote>
<p>Vesper<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_361c009540c961da23345b1e2e1763db.png" alt="image.png"></p>
</blockquote>
<blockquote>
<p>Swann<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_49acef9e82cedd93fd46200d4e1846a2.png" alt="image.png"></p>
</blockquote>
<blockquote>
<p>Armas<br><img src="https://cdn.magichc7.com/static/upload/20220116/upload_1eb1b574761beafeecb34dd003a60dcd.png" alt="image.png"></p>
</blockquote>
<p>在以前的007的时代里，邦女郎就像美酒和香车一样，是消耗品，用过即丢。但是这四位邦女郎，前三位对007产生了巨大的影响。前代的007里，邦德也曾与邦女郎之间有过情感交集。但 Mrs M、Vesper 和 Swann，就像《神探夏洛克》中的艾琳.艾德勒之于福尔摩斯一样，她们是镜子，映射出“完美理性人”身上“不可能存在”的人性。她们与邦德之间的故事，扯开了邦德作为一个冷面杀手的伪装，露出人的真实和复杂。</p>
<h1><a id="toc-25f" class="anchor" href="#toc-25f"></a>总结</h1>
<p>我觉得我很幸运，我看的第一部007系列的电影，正好就是《皇家赌场》，没有被前代布鲁斯南的形象影响，在我心里，007一直就是丹尼尔.克雷格这样的，亦如一些人心中，007就应该是布鲁斯南那样。<br>007系列有很多的缺点，比如剧情的硬伤，比如反派关键时刻的降智。尤其是反派都是一个风格的，那就是不肯好好说话，一定要当谜语人。但它同时也是优秀的商业片，是电影工业的优秀作品。尽管它真的有很多很多的不足，但是生活中有时候不需要那么多的深度，我们需要的就是最直接的视觉刺激，最畅快的快意恩仇，在荧幕里，做一个个生活中实现不了的梦。而且，导演也在传统的间谍片套路以外，做出了自己的尝试，给出了一个不一样的邦德。<br>这五部007，会成为日后我回忆过往的一个媒介。在我经历过人生的复杂后，成为追溯年少时心中的英雄热血的钥匙。<br>丹尼尔.克雷格在自己的最后一场007戏结束后，为自己和邦德做了一段总结，我完整的摘抄在这里。感谢陪伴，期待未来的作品。    </p>
<blockquote>
<p> 这角色我演了很久了。<br>现场应该有一些人是我从影以来就在合作的人。那也快30年了，真的过了很久。很多人跟我在五部电影都合作过。<br>我知道我说过很多次，对这些电影有什么看法，每一部都是。<br>但是我真的爱死了拍一部电影时经历的每一秒钟。<br>尤其是这部，因为我每天早上起床后，都能有幸与你们共事。<br>这是我这辈子最大的荣幸。</p>
</blockquote>

            ]]></description>
            <pubDate>Sun, 16 Jan 2022 08:06:23 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/moviereviews-3.html</guid>
        </item>
        <item>
            <title>2021年的年度总结</title>
            <link>https://blog.magichc7.com/post/review-of-2021.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#anylearn">Anylearn</a></li>
<li><a href="#toc-0be">字节跳动</a></li>
<li><a href="#toc-ddb">其他的一些零碎</a></li>
<li><a href="#toc-433">结语</a></li>
</ul>
</div><p>2021年过去的可真快啊。</p>
<!--more-->
<p>又到了一年一度的年度总结的时间了。</p>
<p>回去翻了一下2020年的年度总结，还是比较丧的。有悔于2020年的蹉跎，亦有对自己的不满意。不满意主要来自于对待工作和人生的态度有些过于随意了，还有对时间的浪费，很多自己计划的事情没做好。</p>
<p>不过我对于2021还是比较满意的，我的成长超过了我在年初时对自己的预期；我做成了我未曾设想能做到的事情，虽然不是年初规划时的那一件；对于过去困扰我很久的一些心结，我也有了解。照例在新旧之际，好好总结一下吧。</p>
<h1><a id="anylearn" class="anchor" href="#anylearn"></a>Anylearn</h1>
<p>在2020年的年度计划里，我给自己定的目标是想推动Anylearn的开源化。不得不说，这确实是一个很难的事情，至今我们也没做到。但是我们也迈出了一步，就是常态化的运营平台，为组内的科研活动做支撑。事实证明，很多灵感和经验必须要从生产数据中才能拿到。停留在PPT上的系统，永远只能停留在PPT上。</p>
<p>在2021年接近收尾的时候，我和大芃又回到了小白板面前，开始做明年的前期规划。我们开始以快慢数据、性能指标等方法分析系统，我们也开始做一些在我看来真的是很难的事情的设计。无论是技术的复杂度还是工程的难度上，都要远甚于以往。事实上，前三年，我们很难回答Anylearn系统的创新点在哪。但是从第四年开始，我相信这个问题的答案将开始出现解，并且会不止一个解。</p>
<p>我们做Anylearn，既是在做一个业务系统，也是在做一个基础架构。Anylearn是一个Sys For ML。为了支撑这样的一个系统，我们用了足足三年的时间，去踩坑去建构。</p>
<p>我们走了很多弯路。曾经我们希望，让Anylearn成为一个工业场景下，对没有算法开发经验的工程师友好的AI模型开发系统。我们提了很多slogan，比如数据通路，比如低代码，乃至更早的数据模式。我们希望能降低全流程的门槛，但是事实就是，失败了。（我发现我越来越能接受自己失败了，也算一种进步吧）   </p>
<p>我觉得问题主要出在两个地方</p>
<p><strong>首先，简单的才是复杂的</strong></p>
<p>一个比较经典的AI降低门槛的思路就是，对通用场景做分类，然后用大量数据构造预训练模型。但是这个思路是有问题的。</p>
<p>AI火起来，一方面是因为硬件的发展。但更重要的因素是数据量的膨胀。毕竟没有足够的数据，现阶段再好的硬件都只是无米之炊。这也是很多论文有漂亮的表格，却无法在真正的生产系统工作的原因。因为用了再多的数据集，跟TB甚至PB级的线上数据比起来，都是那么的不值一提，更别说只在少数几个数据集上凑点的模型了。基于数据归纳的AI方法，与之前基于数据仓库做BI的方法比，一个比较大的进步是人们不需要去自己寻找问题的解，而是可以划出一个求解的范围，然后让模型去收敛出解。    </p>
<p>因此AI模型是特定场景下，对求解过程的抽象和压缩。但是现阶段，很多模型和其参数空间本身就是一个小的混沌系统，AI模型的可解释性问题没有通用解。连专业的AI工程师都未必知道什么超参数靠谱，更别提没有AI知识的普通工程师了。为了解决这个问题，人们又引入了AutoML。目前阶段，AutoML思路都很粗暴，就是搜索+剪枝。而搜索是对资源最不友好的一类算法，本质上还是在拿海量的计算资源去填坑。我觉得要想实现真正的AutoML，首先还是得解决模型的可解释性问题，这样才有可能实现定向的搜索。  </p>
<p>即使当我们成功对用户屏蔽了算法本身的细节后，我们还不得不让用户去理解数据的结构。目前主流的AI算法都是以非结构化的数据作为输入的，用户需要按照算法规定的格式去准备数据。与BI那样以SQL这样的结构化数据作为输入相比，非结构化数据更难以描述，用户很可能在真正把训练跑起来之前，要反复调整文件夹的格式等等。AI系统可以选择把准备数据的这部分成本完全交给用户，或者引入一些数据清洗的工具。这都要求用户是有经验的数据工程师。或者我们提升算法的“智能”程度，再加一个模块，自动理解目录结构，并改成算法可接受的模式，或者做前置判断。我们做过这样的尝试，叫“数据模式”。我并不喜欢它，因为那不是一个可以收敛的解。我们几乎等于在枚举所有的场景，然后人为的去规定目录结构。  </p>
<p>发现没，一个“简单”的AI系统对用户屏蔽的细节越多，那么底层要做的dirty work就越多，系统的复杂性就越高。一旦我们试图在某些地方停下来，提高对用户的要求时，我们的用户画像就自相矛盾了：他既不能有足够的知识，不然他一定会选择自己去虚拟机上写代码训模型，他又要有足够的知识，去解决我们留给他的问题。  </p>
<p>另外，AI系统就是大数据系统，大数据系统又具有其天然的复杂性。其承载的数据量，对存储系统的读写性能、稳定性都有很高的要求。尤其是目前应用比较广泛的图像领域，需要存储系统解决海量小文件场景下随机访问的性能问题。这些如果想对齐AI开发上希望给用户提供的“简单”的体验，要做的事情同样不会少。<br>以上这些复杂度，对于一个小团队来说，是不可接受的。  </p>
<p><strong>其次，缺少场景</strong></p>
<p>在20乃至30年前，学校相对于业界是有国家倾斜的资源优势的，而且软件需要解决的问题主要瓶颈不在规模，而在功能的完整度上和细节的复杂度上。但是在当下，工业界的主要问题的瓶颈都在规模上，尤其是平台式的软件。为什么99.6%的模型比99.5%的模型好，在学术视野下是很难理解这个问题的，但是在工业视野就很好理解。当DAU的单位是以亿计算时，稳定多0.1%的准确率，意味着多十万级的请求能得到正确结果。一个系统设计在二三十个人这样的用户规模下可能勉强能用，偶尔还会出点故障。到了百人千人团队的时候，很可能就是大面积的不可用和故障。一个模型的准确率到底有多稳定，一个系统设计到底经不经得住造，只有在大规模的数据场景下，才能去对科研的结果做验证。学校团队是天然劣势的。</p>
<p>这样两个大问题横在面前，我们最终发现，Easy AI这样的理想，对于我们这样的团队可能很难。所以2021年我们做的最大的调整就是，回归学校的科研场景，提高对用户能力的期待值。当我们放弃对“Easy”的追求后，我们反而同时降低了用户理解的成本和设计的复杂度。这是我们天然熟悉的场景，我们可以实实在在地承载起来，并且在生产中收集数据。而这样的场景是一种长尾的场景，全国可能有很多我们组这样的二三十人的科研团队，这个市场不见得不够大。转型之后，Anylearn的成长速度比以前要快了很多。这离不开大芃和团队其他同学的辛苦付出（salute）。</p>
<p>目前，我们为22年做的一些规划里，已经从上层业务系统的构建，开始向底层基础架构的探索去渗透。我们在21年规划了资源利用率提升的目标，在22年会尝试各种方式去寻找解。这在我看来是非常积极的信号，说明我们开始做一些有望追赶前沿的尝试。诞生于学校的系统是一定不能在业务层面去试图与大厂掰手腕的，只会死的很惨。但是当我们把问题重新从规模化转为一些复杂度的问题时，我们就可以一定程度上去缩小学校和工业界的差距。这其实是借鉴了AI灌水文的思路，如果主赛道玩不转，那就去设置一些很奇怪的偏门settings。  </p>
<p>我自己对基础架构进行了代际上的划分。第一代基础架构是完全基于人治的系统，搭建容易维护难，所有的运维都需要人来做。第二代基础架构是基于固定规则的系统，可以支持一些基于固定策略的自动化运维手段。第三代的基础架构是基于归纳生成的规则运行的Data Driven系统，系统可以分析过往的运行数据，代替人的“经验”去管理系统。而第四代的基础架构，是在大量的经验模型之上，构建出具有强人工智能形态的复杂系统，可以自动发现“经验”中的错误，并加以修正和成长。顺便说一句，关于强人工智能，我个人的判断是：</p>
<ol>
<li>强人工智能一定是诞生于系统构建的实践当中，绝不会是一两个算法模型能做到的。</li>
<li>很多人担心强人工智能出现后会取代人类，我觉得是多虑了。很多人会在强人工智能出现之前就被取代了。</li>
</ol>
<p>Anylearn的上层是Sys for ML，我们希望在基架层面，可以去做ML for Sys。得益于云原生的火车，Anylearn的基础架构可以从第二代开始直接起步，不需要再一步一步的从洪荒时代走过来。我们在这样的基础上，去做一些第三代基础架构的尝试。正因为我们的系统小，一些大规模场景下不可行的解法有可能在这样的规模下是可行的，我们有更广阔的探索空间。希望2022年能有一些好的结果。</p>
<h1><a id="toc-0be" class="anchor" href="#toc-0be"></a>字节跳动</h1>
<p>今年，在字节跳动的实习是我最重要的经历。加入字节跳动给我带来了我意想不到的成长速度，因此我也在秋招季选择了它。  </p>
<p>如果要我问什么样的方式能让一个人成长的最快，那我的答案可能就是把尽可能困难的问题抛给他去解，同时也信任他。在字节，我接到了去解决一个复杂问题的机会，并且也得到了来自同事们的信任。我不知道别的公司是什么情况，但是让一个实习生去组织一个跨团队的项目，这种机会怎么想也不会是俯拾皆是的。</p>
<p>我们做的问题是：如何在物理层面，通过感知业务的拓扑，去构建一个从上到下的完整性，进而做优化。在微服务的体系下，大量的优化实践只关注其物理信息，比如CPU、内存，设备亲和性，碎片率等等，并不关注进程所携带的业务属性。合并部署是其中的一个解，把相关联的业务调度到同机上，以本地通信去替代远程通信，换取链路上的优化。在上层流量面上做流量的规整，向这种本地通信方式倾斜流量，将收益体现出来。</p>
<p>在这里面，基于业务的亲和性做调度是完成了业务拓扑和物理拓扑的对齐，流量调度是完成了业务拓扑和数据拓扑的对齐。而我们还没有完全实现业务、数据、物理这三层拓扑结构完全的对齐，物理拓扑和数据拓扑的对齐是空白的。这也是明年努力的一个点。</p>
<p>在一开始，我只是去做这样的一个方案的预估收益分析。到之后主动去联系各个方向的同学做方案的规划、实现，进而去联系业务做试点、验证。今年春节会有一个初具规模的上线计划，让实战来检验方案的可行性。  </p>
<p>我很感谢最初的自己足够野心勃勃，接住了机会，而不仅仅只是得过且过。实习生可以成为开玩笑时的谈资，但是绝对不应该成为一直寻求帮助逃避成长的借口。这是一个很棒的项目，它不仅锻炼了我coding的能力，更重要的是要求我主动扩大自己的视野，从不同的层面和维度上去思考问题，梳理细节。因为涉及多个团队，我需要主动去理解每个团队是怎么认识和解决问题的。我会时常担心自己的思考会不会因为见得太少而有天然的局限性，因此会主动去与前辈们聊，问他们是如何看待字节基架当中的一些问题的，将自己的想法与整个调度团队的思考对齐，尽可能避免无效的投入，或者做出与大方向相悖的设计决定。在合并部署之外，我也加入了GPU常态混部以及云原生机器学习系统构建这样的方向。一方面是为了能从中学到对设计Anylearn有帮助的经验知识，另一方面也是为了在更难解的问题中，探索成长的空间。</p>
<p>在12月31日，合并部署2021年最后一次周例会上，当我拉着各个方向完整地梳理出合并部署项目在2022年的年度规划之后，我知道跟当初刚加入公司时相比，无论是能力上还是心态上，我都已经有了很多不一样的地方。</p>
<h1><a id="toc-ddb" class="anchor" href="#toc-ddb"></a>其他的一些零碎</h1>
<p>以上两件事是很重要的两块。它们对于我能力和心态的塑造起到了至关重要的作用。也让我对一些过去的心结有了新的看法。比如我的高中生活。    </p>
<p>我其实是很不喜欢高中那段生活的。有比较糟糕的人际关系，有一些很不愉快的回忆。上大学之后的很长一段时间里，我一直保持着对这三年时光的一种恶意，可以说是耿耿于怀，但是我又一直很难说清楚，我到底为什么不喜欢。直到今年，我给这个问题画了一个句号：我对那段生活绝大多数的恶意，都来自于对当时弱小、自大、愚蠢、自私的自己的厌恶。有很多学习之外的事情，比如人际关系，比如情绪控制等等，当时的我都没能很好的处理。我有意或者无意间，给很多人带来了困扰。而当我受到这些行为的反噬时，我甚至没能意识到那些是咎由自取。当下的我能给自己的宽慰就是，现在的我可能有足够的敏锐和能力，去尽可能避免给他人带来负担和烦恼。让这段黑历史不再重演于未来。也许这会与我一贯随心所动的行事风格相悖，但尽可能无扰于人是我所必须恪守的准则。</p>
<p>在时光的间隙中，我也完成了一些小的目标。比如找一份满意的工作，拿到一个不错的薪资水平。比如虽然时断时续，但是保持着健身的习惯，希望能早日降到我初二时的体重水平（140）。比如带着大碗去成都旅行。有人说双人旅行是检验亲密关系最好的一种手段，还好我们的旅程都挺顺利。说起来，当我忙于那些工作的时候，我也很感谢大碗主动静默，尽可能少的打扰我。她是一个很好的陪伴者。为了我所想成就的目标，我牺牲了一些休息的时间，而她牺牲了一部分从我这里的情感的诉求。我只能通过她最喜欢的大吃大喝去弥补她。确实牺牲和弥补不太相称，但是在如今的环境下，这个岁数拒绝躺平选择闷头往前冲可能会让以后的我们感谢现在的自己。</p>
<p>我已经很久没打DOTA2和CSGO了，永劫无间也停在陨星段位后就不打了。倒不是我不会累不需要休息了，而是我发现了比电子竞技更有趣的天梯游戏，那就是人生。  </p>
<p>现实的生活里，是不会有《鬼灭之刃》里面的十二分级，或者是《咒术回战》里面的术师分级这样明确的量化等级的。也许有所谓的大厂技术序列职级来侧面反映，但是终究不会有一个绝对标准。不过，没有可量化的标准不代表无法感知到成长。当我发现我的能力越来越强时，自我肯定带来的喜悦可以带给我甚于游戏上分的喜悦。  </p>
<p>曾经我希望成为一名架构师，现在我对目标做了一些具体化：我希望自己能够成为第三代基础架构资深的构建者，并且在时代进步到第四代基础架构时，成为一个或多个方向的引领者。  </p>
<p>除此之外，明年怎么在更繁忙的安排中更流畅的切换context，更好的处理工作以外的生活是我要关注的问题。毕竟人不是机器，还是要想办法为祖国健康工作五十年的。  </p>
<h1><a id="toc-433" class="anchor" href="#toc-433"></a>结语</h1>
<p>能看到这里，证明你对于我这样一个平凡且无聊的人很关心。谢谢屏幕那边的你，新年快乐。</p>

            ]]></description>
            <pubDate>Fri, 31 Dec 2021 15:08:57 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/review-of-2021.html</guid>
        </item>
        <item>
            <title>一周影评：《扬名立万》</title>
            <link>https://blog.magichc7.com/post/moviereviews-2.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-a96">扬名立万</a></li>
</ul>
</div><p>一场精彩的剧本杀真人秀</p>
<!--more-->
<p>其实这个周末我还看了另一部电影：《老年实习生》。我对这部电影的厌恶已经在朋友圈写过一遍了，纯鸡汤，还是那种没什么营养的，话剧感特别浓，成年人世界的童话。我现在想想，我讨厌它就像我讨厌一些言情偶像剧一样，用虚假的幻影给你以虚假的慰藉。然而被社会毒打一遍，才知道男女之间的爱情根本不是那么回事。</p>
<p>而本周看的另一部电影就很超乎我的预期了，就是万合天宜出品的《扬名立万》。趁着热乎劲再回顾一下吧。</p>
<h1><a id="toc-a96" class="anchor" href="#toc-a96"></a>扬名立万</h1>
<p><img src="https://cdn.magichc7.com/static/upload/20211226/upload_6c973e1000939a4ddadee20d72151565.png" alt="e61190ef76c6a7efce1b4e6e5daab851f3deb48fc860.jpg"></p>
<p>扬名立万是万合天宜出品的喜剧悬疑片。我先摘一下百度百科的剧情简介：</p>
<blockquote>
<p>民国年间，一群失意电影人被召集至神秘之地，参与一部电影的剧本会却发现顾问竟是凶手。几个电影人各自的态度和目的都有不同。案情疑点重重，幕后隐藏着不可告人的真相。他们不知该将一切公之于众还是保持沉默。</p>
</blockquote>
<p>接下来的内容尽量不涉及剧透，仅谈观影感受。</p>
<p>电影的前三十分钟给人的感觉就是一场剧本杀的游戏。桌上什么样的玩家都有：有话特别多的特别爱逞能的自大狂，有头脑简单四肢发达的铁憨憨，有事儿特别多的矫情女玩家，还有一直追着嫌疑人刨根问底的推土机。我也以为，这个片子的悬疑可能是凶手不是那个最像凶手的顾问，而是桌上的另有其人，比如那个关老师。直到最后主要真相揭示前，我都在想会不会主角团里突然有个人掏出手枪说“哈哈没想到吧爷也是凶手”。</p>
<p>好在，这部电影在悬疑的设置上，并没有落入一些烂剧本剧本杀的俗套，玩“给被打晕、迷晕、电晕并被捅了三刀的嫌疑人掐死的那个才是凶手”这种无聊的游戏。柯南已经告诉我们，三选一的选择题看多了就无聊了，让观众猜谁是凶手这种有限解空间的游戏是很难出彩的。因此编剧的选择是——隐藏案件的真相，让观众去推测凶手的犯罪动机。这样可以设置悬疑的空间就会大很多。在电影的海报中，导演还留了另一个给观众的谜题，亦或是提示，那就是海报右下角的那个女人是谁。</p>
<p>这部电影在“反套路”上不止这里。除了上面提到的谜题的设置，很多观众看到袁华跑出去追目标的时候，可能都以为后面会留伏笔拍第二部，那种一路追到最后对方来个邪魅一笑，像《动物世界》一样。但是袁华在最后一刻停住了脚步，因为没有必要了。所有的真相他在见到目标时早已了然于胸，所有的问题他都有答案，又何必再去打扰别人用生命保护的清净。这也是导演的克制，到这里就结束，整部电影就非常的完整，再往下只会自讨苦吃，搞得又臭又长。影片还是在结尾留了小悬念，那就是越南撞船和无人来取的照片。不过我不觉得这可以用来展开一个续集。我个人认为只是导演和观众之间的一个小小的游戏罢了。可以解读成主角团死了，也可以解读成这是另一个掩盖踪迹用的“故事”。</p>
<p>剧情整体的节奏是张弛有度的。只有一个地方我觉得稍微有点拖沓，就是主角团们在选美舞台跳舞的那前后。这一段内容并不是编剧的问题，因为这段剧情既为后面的解密给出了提示，也让凶手利用袁华完成了自己的目标，骗过了所有人，遮掩了最想保护的真相。我觉得可能是节奏转换上的问题吧，之前的节奏都是比较快的，到这里突然慢下来，同时又没什么明面上的信息量，会让人忍不住有想快进的冲动。</p>
<p>扬名立万是电影的叙事主线。主角团是一群失意之人，不惜与杀人凶手共处一室，也要一招翻盘，扬名立万。而还原案件的真相是叙事的暗线。看到后半段，因为暗线非常精彩，遮掩了主线，会有一种跑题的感觉。所以最后主角们把电影拍出来，在我看来有点像为了篇末点题所做的扣题行为，给明线一个结尾。事实上这个结尾怎么处理都不重要了，只需要合理就行。就像是一桌大餐的最后上的那盘水果拼盘一样，给故事一个结尾。</p>
<p>让我们于电影之外，来聊聊导演和演员。我非常惊讶于，这是导演刘循子墨的处女作。完成度非常高，把国内近几年内的悬疑片都放在一起评，这部电影都绝对可以排的上号。刘循子墨之前的作品，主要以微电影和网络剧这样的短剧为主。不得不说，这是一位非常有天分的导演，而他也很年轻，今年才35岁。很期待他之后的作品。</p>
<p>电影的演员都不是什么大牌，要么是万合天宜自家的演员，要么是喻恩泰和小姨妈这种“过气明星”（冒昧冒昧），以及“海王”尹正。剧中的这些角色的囧，又何尝不是这些演员身上正在经历的事情。《武林外传》之后，秀才和小师妹陷入中年危机。《爱情公寓》散场之后，主演们一直被批评走不出角色，只会恰情怀烂饭和玩综艺。这部电影，也是导演和演员们的一次扬名立万。</p>
<p>在商业上，这部电影是非常成功的。这部电影的总成本不过千万级，属于预算非常有限的那种，整个电影的场景很简单，基本就是在一个“风雪庄园”的内部，像是豪华版的密室逃脱。有点像《那个男人来自地球》，预算肉眼可见的紧张，但是剧情却非常精彩。也许我们看这些小而美的电影，就像古人欣赏《核舟记》一样。</p>
<p>最后，放两张我很喜欢的白客的写真。希望曾经带来无数欢乐的《万万没想到》、《夏洛特烦恼》、《武林外传》以及《爱情公寓》的演员们都能扬名立万。</p>
<p><img src="https://cdn.magichc7.com/static/upload/20211227/upload_5de7a8211014abbfd6a9e4b175b2caf0.png" alt="jiycuqft3y3jiycuqft3y3.jpg">
<img src="https://cdn.magichc7.com/static/upload/20211227/upload_100a44cd99ac5812678d054b152e5ff9.png" alt="src=http___b-ssl.duitang.com_uploads_item_201804_26_20180426215618_PYevX.thumb.700_0.jpeg&amp;refer=http___b-ssl.duitang.jpg"></p>

            ]]></description>
            <pubDate>Sun, 26 Dec 2021 14:50:22 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/moviereviews-2.html</guid>
        </item>
        <item>
            <title>一周影评：《贫民窟的百万富翁》和《古董局中局》</title>
            <link>https://blog.magichc7.com/post/moviereviews-1.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-c24">贫民窟的百万富翁</a></li>
<li><a href="#toc-6a4">古董局中局</a></li>
</ul>
</div><p>所有内容均为个人片面的观点，绝不承诺理中客。</p>
<!--more-->
<p>本周看了两部电影，一部经典《贫民窟的百万富翁》，一部新片《古董局中局》。</p>
<p>《贫民窟的百万富翁》，常年霸榜各种必看榜单，与《肖申克的救赎》《黑客帝国》等片子一起被奉为影史经典。而《古董局中局》是新上映的国产悬疑片，演员有我很喜欢的雷佳音和辛芷蕾。两部片子都是女朋友推荐的，下面进入正题。</p>
<h2><a id="toc-c24" class="anchor" href="#toc-c24"></a>贫民窟的百万富翁</h2>
<p><img src="https://cdn.magichc7.com/static/upload/20211219/upload_765968fa51fa9b2feedaf058a3c1c9b7.png" alt="644a5b1552e9c003f6829552f20885e.jpg"></p>
<p>我本以为《贫民窟的百万富翁》是一部在主旨上会挖的很深刻的片子，是怀着去看《阿甘正传》的心情去看的，看完之后的结论是：这是一部在剧情上不错，叙事上很强，整体很流畅的片子，但是在对主旨的表现上，又非常的克制和隐忍。</p>
<p>主线剧情围绕一个贫民窟长大的穷小子通过答题成为百万富翁展开，但是与一般的爽片不同，以倒叙的方式去展开故事。全片利用警察的审问，巧妙地采取了倒叙的手法，而在电影的开头，也通过字幕和标题本身，提出了一个最大的悬疑：为什么一个穷小子能赢取百万，并给出了4个选项：</p>
<ul>
<li>A 他作弊了</li>
<li>B 他很幸运</li>
<li>C 他是天才</li>
<li>D 造化弄人</li>
</ul>
<p>这里造化弄人是我基于个人理解做的翻译。警察盘问主人公贾巴尔的问题——“你为什么知道答案”的答案，是非常痛的。几乎每一道题的答案背后，都是贾巴尔一段很痛苦的回忆：跳入粪坑换来的签名照被哥哥卖掉，母亲因为宗教冲突被杀，被骗入贼窝，心爱的女孩被自己一次一次的抛下，兄弟反目。上帝给了他这些问题的答案，代价是一点一点夺走他仅有的那点幸福。</p>
<p>影片第一个任务是讲一个好的故事，卖出票房。毕竟除了少数比如许鞍华这种叫好不叫座的导演用爱发电，大多数导演还是要用票房成绩回答投资方的期待。而咸鱼翻身这种非常戏剧化的展开，历来都是很容易让大众接受和喜欢的故事。《爱情公寓》光抄这部电影就能抄出来个第三季热搜。</p>
<p>这部电影的主线故事——贫民窟穷小子答对所有题目抱得金钱和美人归，有太多的巧合，结尾又是happy ending，这是非常的戏剧性和超脱现实的，但是会让人看的开心。作者没有像某些蹩脚的导演，一定要在最后喂你一口翔，让你看主人公赢了一路最后人财两空，就像是你一场美梦做的好好的突然一棍子给你打醒一样，或者看到让二追二然后BO1输掉BO5一样，去追求所谓的“现实感”。这就是一场给观众的美梦，让大家都开开心心的看完有什么不好呢。</p>
<p>借助主人公的回忆，影片完成了第二个任务：对印度现实的刻画。主线剧情是戏剧化的，但是不耽误影片依然有其现实感，而不是完全的架空世界。</p>
<p>影片对现实止步于亲历者视角的刻画，这是作者的克制，同时也是因为不便于深入去展开。一来在大多数时间里，主人公都是被哥哥保护的很好的那个孩子，对于现实并不能产生很深刻的理解，一个7岁的孩子突然给你来一段深沉的剖析，醒醒，这不是柯南；二来这是一部商业性质的电影，电影的主色调依然是偏向积极一面的，不宜过分的解读和渲染痛苦；三来，那些呈现的画面已经足够触目惊心，现实就是这么的赤裸裸和血淋淋，已经不需要再解读什么。作者用贾巴尔的嘴说了出来：“你不是想看真实的印度吗？这就是真实的印度！”</p>
<p>影片完成的第三个任务是以主人公本身、男女主之间的关系和兄弟之间的关系去展现人性的美。</p>
<p>贾巴尔本人是这部电影里的光，被现实磨炼出来的世故让他避开了主持人埋下的坑，他懂得欺骗，两个衣着光鲜但本身都是从贫民窟里走出来的人坐在台上较量，贾巴尔更胜一筹。但是即使经历这么多的苦难，贾巴尔依然坚持着寻找拉缇卡。两人那脆弱的爱情，是上天赐予贾巴尔最宝贵的财富。在最后一题，贾巴尔选了A。对他而言选什么已经不重要了，因为电话里的拉缇卡的声音，已经告诉他他得到了自己想要的爱情。这里有两个理解，一种是认为三个人因为玩过家家的游戏，其实都知道第三个火枪手的名字，另一种是不知道，主人公确实是蒙的。我倾向于后者，因为贾巴尔是个诚实的人，雨夜哥哥说的那句你都不知道第三个火枪手叫什么，一个孤儿应该也不会突然掏出手机善用搜索知道答案，他说不知道那么他应该就是不知道。而拉缇卡是之后才加入的第三个火枪手，在兄弟两还沉迷于玩这个角色扮演的游戏的时候，她还不在场。她也确实可能不知道。</p>
<p>兄弟的感情，尤其是哥哥的心理，是以贾巴尔为中心叙事下的一个盲点，就是无法直接呈现哥哥的内心到底在想什么，只能通过人物的行为。哥哥萨利姆是宇智波鼬式的悲情人物，背负了绝大多数的黑暗，完成了自己作为哥哥最重要的任务——保护弟弟。其他的都可以不用管，杀人或者背叛，乃至背叛弟弟也在所不惜，最后为了弟弟的幸福选择牺牲自己。这里我其实是有疑问的，就是经历了那么多黑暗的哥哥，真的还能做出牺牲自己这样的举动吗？前几分钟还在拉缇卡脸上留下了一刀，之后就突然对拉缇卡来一句“原谅我吧”。但是还是回到第一个任务里说的，主线剧情是喜剧，那么就相信哥哥心底依然有那一块柔软吧。</p>
<p>总结一下就是，很不错的片子，无论是作为文艺片还是作为商业片都会得到不错的观影体验。确实无愧于霸榜的地位。</p>
<h2><a id="toc-6a4" class="anchor" href="#toc-6a4"></a>古董局中局</h2>
<p><img src="https://cdn.magichc7.com/static/upload/20211219/upload_108261f2a51c84b4ebd16ef7368a1580.png" alt="d430239b380e9e9f64d4477c89c6836.jpg"></p>
<p>在《上海堡垒》这种垃圾片和大制作的主旋律作品之外，近几年国产的剧情片其实质量是有长进的。不少悬疑片已经能讲好一个悬疑故事不挖坑不填，叙事有基本的段落感。希望这是产业进步的表现，以后能有更多好片子看。</p>
<p>看电影的时候，坐我边上的哥们说，感觉像是在看鬼吹灯。等主人公一行人下地之后，他表示连墓都有了，这更像了。我看完查了一下，才知道《古董局中局》是有原著小说的，主人公是文玩世家。当年泥腿子们的故事并不能被搬上荧幕，搬上荧幕的都已经被招安了，文物都上交给国家了。因此后来的相关题材作品从一开始就很注意立场，避免被合法化之后产生无比巨大的撕裂感。</p>
<p>因为是新电影，我就不像《富翁》一样聊那么细的剧情了，避免剧透。大致就是主人公追查国宝文物下落的故事。我简单说一下剧情的观感和一些我喜欢和我不喜欢的点吧。</p>
<p>剧情上，我觉得比较突兀的点是主人公的转变，从一个酒蒙子和该溜子突然双眼一炬变成文物守护战士。主人公开头流里流气，结尾整洁得体的故事有很多，比较优秀的是《我不是药神》。这部片子里，主人公的性情转变是很慢的，这也符合人本身的惯性。一个从七岁起就感觉自己被父亲抛弃，连遗物都不太想要的人，受到死亡威胁之后，他的本能情绪应该是“愤怒”。但是我觉得不是对敌人的愤怒，而是对父亲的愤怒——“七岁抛下我自己跑了然后自己惹事，死了还让我惹火上身是吧，tmd我房租还没交呢。”</p>
<p>第二个就是最后揭秘真国宝的所在的时候，说实话我晚上看的电影半夜已经记不住是怎么推出来的了，因为我是没感觉那个推理能硬到能给主人公那么坚定的信心让他敢当着那么多人面装逼。除此之外，电影剧情就是盗墓系列电影的经典走向——密室逃脱。在谜题的设置上，《古董》是合格的。而不是隔壁脖子哥的《风起洛阳》：推理方式是，如果不是A，那么就是B，如果不是B，那么就是C，如果不是C，那么就是D。</p>
<p>说到脖子哥，那就正好谈一下表演。我女朋友觉得李现绷着个眉头让她很出戏。男二的设定是类似于《盗墓笔记》中花解语的那种——家族精英，心思深重。我个人的感觉是李现的表演是及格的，我没有在他这里出戏。当然，他的表演本身也乏善可陈。只能说再接再厉。</p>
<p>我最惊喜的还是葛大爷。澡堂里我一开始还没看出来这是葛优，等开始盘问的时候我才发现一个北京出租车司机不好好开出租车来这里保护文物来了。我只能说这些时光打磨出来的老演员真的个个都是珍稀的点金石。</p>
<p>雷佳音和辛芷蕾给我的感觉就是对情绪和细节的处理都是成熟演员应该有的水准，但是也有他们这一代演员身上的通病，就是他对于情绪和细节的处理总是那一套，你在每一部电影里看到的都是雷佳音和辛芷蕾，而不是金士杰老师的老战士、魏忠贤、老父亲，或者李幼斌老师的团长和俊后生科学家。很多老戏骨都有这种演什么是什么的能力。雷佳音他们目前还是演什么像什么。不过新生代演员里，反而有几个演员演不同的角色给人就是不同的感觉，比如王传君，章宇（《我不是药神》里的黄毛），期待他们的成长。</p>

            ]]></description>
            <pubDate>Sun, 19 Dec 2021 15:12:29 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/moviereviews-1.html</guid>
        </item>
        <item>
            <title>分布式系统的一致性——从一个任务的生命周期谈起</title>
            <link>https://blog.magichc7.com/post/distribution-system_s-uniformity.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-fa0">先从一个简单的流程开始</a></li>
<li><a href="#toc-b75">会出现状态不一致的情况吗？</a></li>
<li><a href="#toc-c94">还有什么问题没考虑到吗？</a></li>
<li><a href="#toc-2ff">我们考虑更复杂的情况</a></li>
<li><a href="#toc-915">小小的总结一下</a></li>
<li><a href="#toc-3d1">一些解决办法</a></li>
</ul>
</div><p>最近在做一个Kubernetes上的任务调度系统，做着做着遇到了一些有趣的问题，在这里总结一下。</p>
<!--more-->
<p>在做系统设计的时候，对于我这样的分布式体系的初学者，一般都是从传统的单例开始去思考业务流程，进而设计系统的组件与功能。而分布式系统的一致性保证是一个一开始不会注意到，但是就像你玩吃鸡搜楼时，突然出现的那把喷子一样，一击要命。</p>
<h3><a id="toc-fa0" class="anchor" href="#toc-fa0"></a>先从一个简单的流程开始</h3>
<p>我们要设计一个任务的调度系统，那么首先得明确任务的生命周期有哪几个阶段<br><img src="https://cdn.magichc7.com/static/upload/20210820/upload_96e2f29f35fb8769c3a9e176ec1f520f.png" alt="简单的任务生命周期.jpeg"><br>这是一个简单的四元状态，任务从创建开始，有完成和失败这两个终点。那么我们首先应该有一个数据库来记录这些状态，有个后端来读写。当然，并不是数据库有了一条记录，任务就跑完了，我们得实际的让任务跑起来，所以还得有个执行引擎，这里就是Kubernetes。<br><img src="https://cdn.magichc7.com/static/upload/20210821/upload_9db93d4c1ecc3f489bc86e75419334da.png" alt="简单的系统组件.jpeg">  </p>
<h3><a id="toc-b75" class="anchor" href="#toc-b75"></a>会出现状态不一致的情况吗？</h3>
<p>在设计系统的时候，我们不能假设所有的组件，甚至包括网络环境是永远稳定的。事实上，大部分时间，包括网络这种基础环境可能都是不稳定的。当然，稳定性或者说可用性会有一个排序，可以先假设一些组件是高可用的，比如基础网络，不然这个系统设计就没法做了。<br>上面的系统设计里有一个问题，任务状态的写回是依靠容器内部的业务进程自己发起HTTP请求来告诉后端自己的任务执行情况的，是失败了还是成功了。但是，如果后端不可靠，给了一个错误的写回地址，或者容器内的业务进程实际完成了工作，但是没有执行写回操作，那么业务的状态就会一直保持为运行中，这与实际的负载情况是不一致的。<br>那么有没有什么办法可以避免这个问题呢？如果是在Kubernetes里，那么其实可以依靠监控容器的状态来确定任务的状态。在Kubernetes里，Pod可以有五个Status Phase，分别是Pending，Running，Succeeded，Failed，Unknown。这里，如果容器进程正常退出的话，就是Succeeded，异常退出就是Failed。我们可以利用这一点，不去Catch各种异常情况，当任务Crash的时候，就让它直接Crash掉。依靠Kubernetes来告诉我们这个业务的真实状态，本质上是用一种可用性更高的组件替换了可用性低的组件，来提升整体的可用性。</p>
<h3><a id="toc-c94" class="anchor" href="#toc-c94"></a>还有什么问题没考虑到吗？</h3>
<p>我们到现在为止都是在假设只有一个后端实例的前提下做的设计。提升系统可用性的一个手段是多副本，那么如果实例数变多，会有什么问题呢？我们看一下改为多副本后的架构。<br><img src="https://cdn.magichc7.com/static/upload/20210821/upload_9061a56e5bffc8b5c9c2f26be9a8756d.png" alt="稍微复杂一点的系统组件.jpeg"><br>发现问题了吗？如果容器完成之后，Kubernetes会给每一个后端都通知一次这个Pod状态变成Succeeded或者Fail的事件，那么每个后端都会写一次数据库。我们可以相信Kubernetes是高可用的，不会出错的，那么就可以假设，通知容器状态这里是完全同步且通知的状态都一致的。那这里最大的问题就是，可能会出现重复写数据库的情况，有多少后端一个任务就会写多少次数据库。当然，假设数据库的承载能力无限，那这里其实也没什么大问题。</p>
<h3><a id="toc-2ff" class="anchor" href="#toc-2ff"></a>我们考虑更复杂的情况</h3>
<p>刚才的任务生命周期只有四种简单的状态。不过，执行任务是要消耗资源的，资源是有可能不足的，任务执行也是可能会失败的，甚至用户可能跑一半不想跑了，任务是会被取消的，失败和取消的任务是可能重试的。让我们看一个更完整的任务生命周期图。<br><img src="https://cdn.magichc7.com/static/upload/20210821/upload_ce4eab67795b9bc63cfdfc4b2470c20c.png" alt="复杂的任务生命周期.jpeg"><br>加入资源之后，我们又要考虑，资源的申请与释放。因此，刚才的架构图又有了新的内容。<br><img src="https://cdn.magichc7.com/static/upload/20210821/upload_d78e454edc7522d52cd51b486bf62636.png" alt="复杂一点的系统组件.jpeg"><br>任务状态从已创建和待重试变成Pending态（启动容器，配置网络等，对应于Pod的Pending Phase）要占住资源，运行中变成成功、失败、取消时要释放资源。如果是单副本，一切都没有问题，但是，如果是多副本呢？我们仅看释放资源这里，现在每个后端不仅仅是往数据库里写一次任务状态了，他们还要增加资源的余量。于是，用户惊喜的发现，每跑完一次任务，可用剩余资源都增加了！（这倒是可以鼓励用户多跑任务）。<br>除此之外，还有一个可能产生不一致的地方，就是，每个后端都是要先从数据库读取当前的剩余，本地计算完新的余额后，再写入数据库。如果这个读-&gt;改-&gt;写没有做成原子事务的话，那么甚至有可能，对于有x个后端的系统，假设每个后端预期是释放1单位资源，最后增加的资源总量是小于x的（乐观点看这倒是让资源增加的没那么多）。</p>
<h3><a id="toc-915" class="anchor" href="#toc-915"></a>小小的总结一下</h3>
<p>实际上，在资源的申请阶段也会有不一致问题。分布式系统中我们都知道可能有不一致的问题，但是有没有什么办法帮助我们快速诊断一个系统中有没有不一致问题呢？我个人的观点是这样的：  </p>
<blockquote>
<p>在系统中，出现了数据拷贝与分发环节，且分发的数据被后续组件有外部性的处理时，就可能会产生不一致的问题。</p>
</blockquote>
<p>这里着重提一下，什么是外部性以及为什么要强调产生外部性。外部性就是这个组件基于这个数据，修改了系统内其他组件的状态，而如果这些分发的数据没有被有外部性的处理，比如只是存起来，在假设传输过程完全可靠的情况下，就不会有问题。那么可能有朋友会说，那传输不可靠呢？那这里其实就应该把网络设备也当成系统的组件来看待，网络设备接收了数据并传递给其他组件修改他们存储的状态，这就产生了外部性，自然，也会有不一致性问题。</p>
<h3><a id="toc-3d1" class="anchor" href="#toc-3d1"></a>一些解决办法</h3>
<p>当然，可能产生不一致我们就要想办法去消除。有些可以简单想到的方案，比如说，既然是因为分发导致的，那么我就不分发好了，我只让一个后端来处理任务的资源申请和释放。这就是选主的思路，本质上是把后端又变成只有一个。另外的方式就是加锁，比如参考Kubernetes，做版本控制。但是加锁其实会引入很多性能的问题，所以如果能从系统设计的角度避免问题，我更倾向于设计更优雅的架构而不是用各种锁去强行保持强一致性。</p>

            ]]></description>
            <pubDate>Fri, 20 Aug 2021 15:38:37 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/distribution-system_s-uniformity.html</guid>
        </item>
        <item>
            <title>Elasticsearch避坑系列</title>
            <link>https://blog.magichc7.com/post/debug-record-about-eck.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-758">日志乱序</a></li>
<li><a href="#toc-98e">磁盘打满</a></li>
<li><a href="#toc-b21">查询过慢</a></li>
<li><a href="#toc-e20">filebeat pod反复重启</a></li>
</ul>
</div><p>在这里记录一下使用Elasticsearch全家桶时遇到的各种各样的问题</p>
<!--more-->

<h3><a id="toc-758" class="anchor" href="#toc-758"></a>日志乱序</h3>
<p>最近在开发的时候，同事反馈出现了日志乱序的问题。原先我们按照offset进行排序和锚点，但是打印出来的日志发现，offset与timestamp的大小关系不一致了：<br><img src="https://cdn.magichc7.com/static/upload/20210806/upload_d61f5cbac6bb16eda29bebe1472ba547.png" alt="image.png"></p>
<p>经过排查发现，因为打印的日志太多了，导致超过containerd单日志文件offset限制了。<br><img src="https://cdn.magichc7.com/static/upload/20210806/upload_7856ea3190425bc0f141a71b0048c0b2.png" alt="image.png"></p>
<p>此时Containerd会把软连接指向的日志文件从默认的0.log更新到1.log，导致offset重新计算。现在先换成使用@timestamp做排序和锚点了。</p>
<h3><a id="toc-98e" class="anchor" href="#toc-98e"></a>磁盘打满</h3>
<p>默认安装的eck集群没有配置close_timeout选项，导致大量的文件句柄没有被释放。占满磁盘。<br><img src="https://cdn.magichc7.com/static/upload/20210808/upload_3f8b31e4f3cc8e272e7986ebf5987ef5.png" alt="image.png"><br>具体原因：<a href="https://blog.csdn.net/weixin_33775582/article/details/89651291">https://blog.csdn.net/weixin_33775582/article/details/89651291</a></p>
<h3><a id="toc-b21" class="anchor" href="#toc-b21"></a>查询过慢</h3>
<p>曾经出现过elasticsearch查日志特别慢，后来发现是因为没有定期清理日志索引，导致日志索引过大。</p>
<h3><a id="toc-e20" class="anchor" href="#toc-e20"></a>filebeat pod反复重启</h3>
<p>配置的resource limit过小导致的</p>

            ]]></description>
            <pubDate>Sun, 08 Aug 2021 13:36:10 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/debug-record-about-eck.html</guid>
        </item>
        <item>
            <title>Operator开发过程中的Reconcile Conflict问题</title>
            <link>https://blog.magichc7.com/post/solve-conflict-in-operator-reconcile.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-5dc">问题</a></li>
<li><a href="#toc-887">成因</a></li>
<li><a href="#toc-de8">解决方案</a></li>
</ul>
</div><p>在使用KubeBuilder开发Kubernetes Operator的时候，会出现资源过期问题。本文解释成因及解决方案。</p>
<!--more-->
<h1><a id="toc-5dc" class="anchor" href="#toc-5dc"></a>问题</h1>
<p>端午节我在寝室给一个项目闷头写Operator的时候。设计了这样的一个用于创建Pod一次性执行特定任务的CRD。大概过程是，CR创建时，应该按照Spec的内容构建一个Pod，然后在Pod进入Failed阶段就重试，重试多次后标记任务失败，Complete就标记为任务完成。伪代码如下：</p>
<pre><code class="hljs lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *TrainTaskReconciler)</span> <span class="hljs-title">Reconcile</span><span class="hljs-params">(ctx context.Context, req ctrl.Request)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    reqLogger := log.FromContext(ctx).WithValues(<span class="hljs-string">"TrainTask"</span>, req.NamespacedName, <span class="hljs-string">"rand"</span>, rand.Float64())

    <span class="hljs-comment">// 获取当前的TrainTask的定义</span>
    <span class="hljs-keyword">var</span> trainTask anylearnv1.TrainTask
    <span class="hljs-keyword">if</span> err := r.Get(ctx, req.NamespacedName, &amp;trainTask); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-comment">// reqLogger.Info("unable to fetch TrainTask")</span>
        <span class="hljs-keyword">return</span> ctrl.Result{}, client.IgnoreNotFound(err)
    }
    <span class="hljs-keyword">if</span> trainTask.Status.State == <span class="hljs-string">"Finished"</span> || trainTask.Status.State == <span class="hljs-string">"Failed"</span> {
      <span class="hljs-comment">// 已经结束的任务没有必要再启动</span>
      <span class="hljs-comment">// reqLogger.Info("TrainTask is over", "state", trainTask.Status.State)</span>
        <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
    }
    <span class="hljs-comment">// 查看当前TrainTask定义的Pod是否存在</span>
    <span class="hljs-keyword">var</span> taskPods v1.PodList
    <span class="hljs-keyword">if</span> err := r.List(ctx, &amp;taskPods, client.InNamespace(req.Namespace), client.MatchingFields{trainTaskOwnerKey: req.Name}); err != <span class="hljs-literal">nil</span> {
        reqLogger.Error(err, <span class="hljs-string">"unable to list child Pods"</span>)
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    createNewPod := <span class="hljs-literal">false</span>
    statusNeedUpdate := <span class="hljs-literal">false</span>

    <span class="hljs-comment">// 判断当前是否需要创建一个新的容器来运行任务</span>
    <span class="hljs-comment">// 判读状态是否需要更新，之所以要先判断再更新而不是直接更新是为了避免过多的对APIServer的写入</span>
       {
            <span class="hljs-comment">// 检查是否需要创建新Pod以及更新状态</span>
            createNewPod = <span class="hljs-literal">true</span>
            statusNeedUpdate = <span class="hljs-literal">true</span>
       }


    <span class="hljs-keyword">if</span> statusNeedUpdate {
          <span class="hljs-comment">// 这个地方会出Conflict</span>
        <span class="hljs-keyword">if</span> err := r.Status().Update(ctx, &amp;trainTask); err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> ctrl.Result{}, err
        }
    }

    <span class="hljs-keyword">if</span> createNewPod {
            <span class="hljs-comment">// 创建新容器，删除老容器...</span>
    }

    <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
}</code></pre>
<p>在更新状态的步骤，可能会出现Error：</p>
<pre><code class="hljs lang-undefined">Operation cannot be fulfilled on [Resource Kind\Resource Name]: the object has been modified; please apply your changes to the latest version and try again
</code></pre>
<h1><a id="toc-887" class="anchor" href="#toc-887"></a>成因</h1>
<p>下面来解释一下原因以及解决方案：
<img src="https://cdn.magichc7.com/static/upload/20210615/upload_e98eac1439b1bae204c6fcd28cbcf050.png" alt="kubebuilder Read and Write.png"></p>
<p>ResourceVersion会在Resource的任意字段被修改时更新，包括Metadata，Spec和Status。
Kubebuilder内部的实现中，读是用APIReader走缓存的，写是直接走APIServer写ETCD的。其中从APIServer到ETCD的步骤是原子操作，<strong>但是从ETCD到Cache这整个链路并不是原子的</strong>。</p>
<p>两个连续的Event可能会出现，A向APIServer提交Update或者Patch修改了资源对象，ETCD确认了更新（这里是原子的），但是还没来得及将新的资源inform给缓存，B紧随其后从缓存中读到的是一个旧版本，再向ETCD更新时，ETCD会拒绝这次更新。</p>
<p>资源Conflict在Kubernetes中是很常见的错误，原因就是Kubernetes为了提高吞吐，在全局很多地方都采用乐观锁的设计。</p>
<h1><a id="toc-de8" class="anchor" href="#toc-de8"></a>解决方案</h1>
<p>解决方案也很简单，在Update时，如果发现是ConflictError就使用Requeue: True重新执行这个Event，或者在Conflict发生时使用Kubernetes提供的retry.RetryOnConflict。但是，在retry时，注意请根据新Get到的资源版本重新计算状态，而不是Get了一个新的资源对象然后直接拿之前基于旧资源对象算出的状态去Update。个人觉得既然都要基于新的资源对象重新计算状态了，干脆Requeue是最好的。我比较推荐的Reconcile写法如下：
<img src="https://cdn.magichc7.com/static/upload/20210615/upload_6113d4cb2b0f7345782414b1d36fa62b.png" alt="reconcile顺序.jpeg"></p>
<pre><code class="hljs lang-go"><span class="hljs-keyword">if</span> statusNeedUpdate {
  <span class="hljs-comment">// 在获取并更新状态前什么都不要做，如果更新失败就说明手里拿到的已经是一个老的版本了，就重新排队</span>
        <span class="hljs-keyword">if</span> err := r.Status().Update(ctx, &amp;trainTask); err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">if</span> apierrors.IsConflict(err) {
                <span class="hljs-keyword">return</span> ctrl.Result{Requeue: <span class="hljs-literal">true</span>}, <span class="hljs-literal">nil</span>
            } <span class="hljs-keyword">else</span> {
                reqLogger.Error(err, <span class="hljs-string">"unexpected error when update status"</span>)
                <span class="hljs-keyword">return</span> ctrl.Result{}, err
            }
        }
    }
</code></pre>
<p>我参考了其他Operator的写法，ElasticSearch的Operator在出现Conflict的时候，也采用了直接Requeue的做法：</p>
<p><a href="https://github.com/elastic/cloud-on-k8s/blob/f52ca4700f56238c56186f0fc751ada563cf6dda/pkg/controller/beat/common/reconcile.go#L73">Beat</a></p>
<pre><code class="hljs lang-go">    err = updateStatus(params, ready, desired)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &amp;&amp; apierrors.IsConflict(err) {
        params.Logger.V(<span class="hljs-number">1</span>).Info(
            <span class="hljs-string">"Conflict while updating status"</span>,
            <span class="hljs-string">"namespace"</span>, params.Beat.Namespace,
            <span class="hljs-string">"beat_name"</span>, params.Beat.Name)
        <span class="hljs-keyword">return</span> results.WithResult(reconcile.Result{Requeue: <span class="hljs-literal">true</span>})
    }
</code></pre>
<p><a href="https://github.com/elastic/cloud-on-k8s/blob/f52ca4700f56238c56186f0fc751ada563cf6dda/pkg/controller/elasticsearch/elasticsearch_controller.go#L196">ElasticSearch</a></p>
<pre><code class="hljs lang-go">    err = r.updateStatus(ctx, es, state)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">if</span> apierrors.IsConflict(err) {
            log.V(<span class="hljs-number">1</span>).Info(<span class="hljs-string">"Conflict while updating status"</span>, <span class="hljs-string">"namespace"</span>, es.Namespace, <span class="hljs-string">"es_name"</span>, es.Name)
            <span class="hljs-keyword">return</span> reconcile.Result{Requeue: <span class="hljs-literal">true</span>}, <span class="hljs-literal">nil</span>
        }
        k8s.EmitErrorEvent(r.recorder, err, &amp;es, events.EventReconciliationError, <span class="hljs-string">"Reconciliation error: %v"</span>, err)
    }
    <span class="hljs-keyword">return</span> results.WithError(err).Aggregate()
</code></pre>

            ]]></description>
            <pubDate>Tue, 15 Jun 2021 03:28:08 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/solve-conflict-in-operator-reconcile.html</guid>
        </item>
        <item>
            <title>Kubernetes CNI系列（2）</title>
            <link>https://blog.magichc7.com/post/Kubernetes-cni-series-2.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-4d1">Calico的工作原理</a></li>
<li><a href="#toc-3b6">引用</a></li>
</ul>
</div><p>继续<a href="https://blog.magichc7.com/post/Kubernetes-cni-series-1.html">上一次的内容</a>，讲解Calico的工作原理。</p>
<!--more-->

<h1><a id="toc-4d1" class="anchor" href="#toc-4d1"></a>Calico的工作原理</h1>
<p>Calico的默认组网方案是一个纯三层方案，所有的包以ip跳转的方式传达。首先看一下Calico所包含的主要组件：</p>
<ul>
<li>Felix： Calico agent，跑在每台需要运行 workload 的节点上，主要负责配置路由及 ACLs 等信息来确保 endpoint 的连通状态；</li>
<li>etcd： 分布式键值存储，主要负责网络元数据一致性，确保 Calico 网络状态的准确性；</li>
<li>BGPClient(BIRD)： 主要负责把 Felix 写入 kernel 的路由信息分发到当前 Calico 网络，确保 workload 间的通信的有效性；</li>
<li>BGP Route Reflector(BIRD)： 大规模部署时使用，摒弃所有节点互联的 mesh 模式，通过一个或者多个BGP Route Reflector来完成集中式的路由分发；
[1]</li>
</ul>
<p>从组件的功能可以看出，在Calico组织出来的网络模型下，任意两个Pod都以数据包跳转的方式完成通信，而无需重新封包解包。且这里无论是本机还是跨机器访问，都是以路由表跳转的方式完成的。我们来进一步看，Calico到底是怎么做到的。</p>
<p>我们先考虑第一个问题，同机Pod之间如何实现路由。</p>
<p>在Flannel的方案下，同机不同Pod之间直接通过cni0网桥通信即可。Calico中看起来并没有这样的网桥设计。我们先看Calico为容器配置了什么样的网络。[2]</p>
<pre><code class="hljs lang-undefined">$ ip a
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
3: eth0@if771: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1440 qdisc noqueue state UP
    link/ether 66:fb:34:db:c9:b4 brd ff:ff:ff:ff:ff:ff
    inet 172.17.8.2/32 scope global eth0
       valid_lft forever preferred_lft forever
</code></pre>
<p>那么，按理来说，容器内流量访问容器外时，应该都会走eth0网卡，默认路由地址应该是走eth0。但是，我们看一下Calico为容器配置了一个什么样的路由规则：</p>
<pre><code class="hljs lang-undefined">$ ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
</code></pre>
<p>嘿嘿，有意思吧，默认网关地址是一个不存在于容器里的地址。这个地址是一个B类地址的保留地址，是不可能在集群里找到的。当一个数据包的目的地址不是本机时，就会查询路由表，从路由表中查到网关后，它首先会通过 ARP 获得网关的 MAC 地址，然后在发出的网络数据包中将目标 MAC 改为网关的 MAC，而网关的 IP 地址不会出现在任何网络包头中。也就是说，没有人在乎这个 IP 地址究竟是什么，只要能找到对应的 MAC 地址，能响应 ARP 就行了。那么，当容器开始通过eth0网卡发送ARP包的时候，首先对端的cali设备会收到请求，但是这个网卡是没有ip地址的，正常来说，也不可能会回应ARP报文。</p>
<pre><code class="hljs lang-undefined">$ ip addr
...
771: calicba2f87f6bb@if4: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1440 qdisc noqueue state UP group default
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 14
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever
...
</code></pre>
<p>但是，这张网卡开启了ARP代理功能，这是一种合法的MAC欺诈技术，一般用于多子网通信。可以看引文3和引文4获取更加详细的解释。简单来说，开启了ARP代理功能的网卡在收到非本网段的ARP请求时，会返回自身的MAC地址，之后收到ARP回应的设备在发送包时，将由该网卡根据自身所在的协议栈的路由表来决定下一跳的地址。这里，对端设备将自身的MAC地址返回给了容器。</p>
<pre><code class="hljs lang-undefined">$ ip neigh
169.254.1.1 dev eth0 lladdr ee:ee:ee:ee:ee:ee REACHABLE
</code></pre>
<p>那么容器就会把包发给veth设备对，进而让其来根据路由表决定下一跳的地址。如果不开启ARP代理，那么veth收到这个包会直接丢掉。<strong>这实际上是把主机当成了一个网关设备</strong>。读到这你可能会疑惑了，为什么要这么做。首先，因为没有网桥，那么当包从容器内传到主机空间的设备对对端的时候，没有网桥设备来处理包的转发，就需要veth自己根据路由表来决定下一跳的地址，因此需要开启ARP代理。那这么做的好处又是什么呢？这个问题是Calico官方常见问题的第三问[5]：</p>
<blockquote>
<p><strong>Why does my container have a route to 169.254.1.1?</strong></p>
</blockquote>
<blockquote>
<p>In a Calico network, each host acts as a gateway router for the workloads that it hosts. In container deployments, Calico uses 169.254.1.1 as the address for the Calico router. By using a link-local address, Calico saves precious IP addresses and avoids burdening the user with configuring a suitable address.</p>
</blockquote>
<blockquote>
<p>While the routing table may look a little odd to someone who is used to configuring LAN networking, using explicit routes rather than subnet-local gateways is fairly common in WAN networking.</p>
</blockquote>
<p>简单理解一下就是，这样所有容器里不需要去根据容器IP去特殊配置路由规则了，直接使用169.254.1.1这一条默认规则就行了。</p>
<p>到这里你也就能理解为什么说Calico是一个纯三层方案了。在Flannel里，本地Pod之间的通信是通过网桥设备完成的，这是一个二层设备，记录的都是Mac地址。而Calico里，本机Pod之间的通信是通过路由表的ip实现的跳转，Mac地址完全用不上。Calico也提供了解释[5]:</p>
<blockquote>
<p>In some setups the kernel is unable to generate a persistent MAC address and so Calico assigns a MAC address itself. Since Calico uses point-to-point routed interfaces, traffic does not reach the data link layer so the MAC Address is never used and can therefore be the same for all the cali* interfaces.</p>
</blockquote>
<p>而当包到达cali设备上，开始在路由表中寻址的时候，如果是本机Pod，那么路由表中会有相关Pod地址与对应的cali设备信息，直接发送即可。如果不是本机Pod，Calico提供了两种方式实现跨节点通信[1][6]。（6这篇引文前面讲的有点问题，只要看它讲ipip那部分就行了）</p>
<ul>
<li><p><code>IPIP</code>
从字面来理解，就是把一个IP数据包又套在一个IP包里，即把 IP 层封装到 IP 层的一个 tunnel，看起来似乎是浪费，实则不然。它的作用其实基本上就相当于一个基于IP层的网桥！一般来说，普通的网桥是基于mac层的，根本不需 IP，而这个 ipip 则是通过两端的路由做一个 tunnel，把两个本来不通的网络通过点对点连接起来。ipip 的源代码在内核 net/ipv4/ipip.c 中可以找到。</p>
</li>
<li><p><code>BGP</code>
边界网关协议（Border Gateway Protocol, BGP）是互联网上一个核心的去中心化自治路由协议。它通过维护IP路由表或‘前缀’表来实现自治系统（AS）之间的可达性，属于矢量路由协议。BGP不使用传统的内部网关协议（IGP）的指标，而使用基于路径、网络策略或规则集来决定路由。因此，它更适合被称为矢量性协议，而不是路由协议。BGP，通俗的讲就是讲接入到机房的多条线路（如电信、联通、移动等）融合为一体，实现多线单IP，BGP 机房的优点：服务器只需要设置一个IP地址，最佳访问路由是由网络上的骨干路由器根据路由跳数与其它技术指标来确定的，不会占用服务器的任何系统</p>
</li>
</ul>
<h1><a id="toc-3b6" class="anchor" href="#toc-3b6"></a>引用</h1>
<ul>
<li>[1] 江公子. <a href="https://blog.csdn.net/jiang_shikui/article/details/85870560">calico网络原理分析</a>. CSDN</li>
<li>[2] 云原生实验室. <a href="https://zhuanlan.zhihu.com/p/75933393">Calico 网络通信原理揭秘</a>. 知乎</li>
<li>[3] wx607823dfcf6a9. <a href="https://blog.51cto.com/u_15169172/2795105">代理ARP：合法的MAC欺诈技术</a>. 51CTO</li>
<li>[4] 紫枫术河. <a href="https://www.cnblogs.com/liushuhe1990/articles/11137530.html">linux 上网卡转发 相互ping，代理arp的问题</a>. 博客园</li>
<li>[5] calico. <a href="https://docs.projectcalico.org/reference/faq#why-does-my-container-have-a-route-to-16925411">Frequently asked questions</a>. Calico blog</li>
<li>[6] 克里斯朵夫李维. <a href="https://juejin.cn/post/6896022422865215495">Calico原理</a>. 掘金</li>
</ul>

            ]]></description>
            <pubDate>Thu, 10 Jun 2021 15:00:17 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/Kubernetes-cni-series-2.html</guid>
        </item>
        <item>
            <title>Kubernetes CNI系列（1）</title>
            <link>https://blog.magichc7.com/post/Kubernetes-cni-series-1.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-065">Linux命名空间</a></li>
<li><a href="#toc-9f5">Docker的网络模型</a></li>
<li><a href="#toc-525">二层网络和三层网络</a></li>
<li><a href="#flannel">Flannel</a></li>
<li><a href="#toc-3b6">引用</a></li>
</ul>
</div><p>从Linux网络模型开始讲Kubernetes的网络通信原理，本节讲Linux网络命名空间、Docker网络模型以及Flannel通信原理。</p>
<!--more-->
<p>对于一个刚接触Kubernetes的新手来说，可能最费解的地方就是，为什么Kubernetes提供了那么多强大的功能，唯独不提供网络支持，而是让用户自己去指定网络实现。实际上，不同公司对于网络模型有不同的设计想法。网络，尤其是ip地址的规划，一定程度上体现了公司基础设施的规划方式。比如有些公司会指定某个子网下的所有终端（服务器、NAS设备等）用于提供数据服务，这些终端设备往往也在地理上邻近。一种很常见的做法是，把不同的DC通过网段区分开。因此，可以认为网络的规划是一种业务的体现，而基础设施是服务于业务的，因此将与业务深度绑定的网络构建工作完全交给用户是一种合理的做法。</p>
<blockquote>
<p>Kubernetes对网络的唯一要求就是所有的Pod之间必须直接可达。当我们聊CNI怎么构建网络的时候，是不需要考虑Kubernetes的。Kubernetes等用户把网络模型构建完成后才入场构建更上层的建筑，创建新容器时，利用/opt/cni/bin下的可执行文件为容器分配网络，构建逻辑依然是由CNI本身决定的。</p>
</blockquote>
<h1><a id="toc-065" class="anchor" href="#toc-065"></a>Linux命名空间</h1>
<p>容器技术所提供的隔离性来自于Linux Namespace和CGroups。前者提供了隔离性，后者提供了资源的分配与限额。本文内容只涉及到Namespace中网络的部分。Linux一共提供了六种命名空间来实现不同的隔离需求。</p>
<div style="overflow:auto;width:100%">
<table width="auto" style="white-space:nowrap"> <thead> <tr> <th>命名空间</th> <th>描述</th> <th>作用</th> <th>备注</th> </tr> </thead> <tbody><tr> <td>进程命名空间</td> <td>隔离进程ID</td> <td>Linux通过命名空间管理进程号，同一个进程，在不同的命名空间进程号不同</td> <td>进程命名空间是一个父子结构，子空间对于父空间可见</td> </tr> <tr> <td>网络命名空间</td> <td>隔离网络设备、协议栈、端口等</td> <td>通过网络命名空间，实现网络隔离</td> <td>docker采用虚拟网络设备，将不同命名空间的网络设备连接到一起</td> </tr> <tr> <td>IPC命名空间</td> <td>隔离进程间通信</td> <td>进程间交互方法</td> <td>PID命名空间和IPC命名空间可以组合起来用，同一个IPC名字空间内的进程可以彼此看见，允许进行交互，不同空间进程无法交互</td> </tr> <tr> <td>挂载命名空间</td> <td>隔离挂载点</td> <td>隔离文件目录</td> <td>进程运行时可以将挂载点与系统分离，使用这个功能时，我们可以达到 chroot 的功能，而在安全性方面比 chroot 更高</td> </tr> <tr> <td>UTS命名空间</td> <td>隔离Hostname和NIS域名</td> <td>让容器拥有独立的主机名和域名，从而让容器看起来像个独立的主机</td> <td>主要目的是独立出主机名和网络信息服务（NIS）</td> </tr> <tr> <td>用户命名空间</td> <td>隔离用户和group ID</td> <td>每个容器内上的用户跟宿主主机上不在一个命名空间</td> <td>同进程 ID 一样，用户 ID 和组 ID 在命名空间内外是不一样的，并且在不同命名空间内可以存在相同的 ID</td> </tr> </tbody></table>
</div>

<p>在 Linux 中，网络名字空间可以被认为是隔离的拥有单独网络栈（网卡、路由转发表、iptables）的环境。网络名字空间经常用来隔离网络设备和服务，只有拥有同样网络名字空间的设备，才能看到彼此。从逻辑上说，网络命名空间是网络栈的副本，有自己的网络设备、路由选择表、邻接表、Netfilter表、网络套接字、网络procfs条目、网络sysfs条目和其他网络资源。从系统的角度来看，当通过clone()系统调用创建新进程时，传递标志CLONE_NEWNET将在新进程中创建一个全新的网络命名空间。[1]</p>
<p>容器的网络，实际上就是一个独立的网路命名空间。那么理论上，如果我们不做任何事，容器应该是完全独立的，宿主机感知不到其网络的存在。如果没有挂载宿主机的文件目录的话，我们甚至无法通过Unix Domain Socket与其中的进程通信。而且，这个命名空间中，并不存在任何真实的物理网卡，因此也不可能绕过宿主与外界通信。</p>
<p>为了让不同的网络命名空间能通信，Linux设计了Veth设备对。它成对出现，以网络设备的形式出现在命名空间中。我们可以理解为这是一条网线，把两个命名空间连起来。发往其中一个Veth设备的数据会被发送到另一个Veth上。这样，我们就能实现不同网络命名空间中的通信。对于容器网络，通常容器中的eth0网卡就是这样的一个设备对的一端。在初始化容器网络的时候，为命名空间内分配设备对的一端，为其分配ip，再为容器添加默认路由规则，最后在宿主机上通过添加NAT或者其他方式让容器能与外界通信。</p>
<p><img src="https://cdn.magichc7.com/static/upload/20210608/upload_39dbd02ff2d787828e7595ee5a6fd6b8.png" alt="网络模型.png"></p>
<h1><a id="toc-9f5" class="anchor" href="#toc-9f5"></a>Docker的网络模型</h1>
<p>Docker提供了四种网络通信方式，分别是Host模式，网桥模式，Container模式，还有None模式。实际上，就是不同的分配网络空间的方式。None模式是只创建了一个命名空间，然后添加了一个回环网卡（lo），使其能ping通127.0.0.1这个地址，就是第一段提到的那种完全隔离的状态。Host模式则是不为容器创建新的命名空间，而是直接让容器与宿主机共享同一个命名空间。容器可以直接使用宿主机的网络设备进行通信。Container模式则是让新的容器共享一个既有容器的网络命名空间。就像Kubernetes中，一个Pod的不同Container之间可以直接使用127.0.0.1互相访问进程一样。</p>
<p>网桥模式是Docker默认提供的网络模式。Linux网桥是真实世界的交换机的一种虚拟实现，在系统中可以用于沟通多个不同的虚拟机网络和容器网络。注意，二者的连接方式通常不同，虚拟机下，主机eth0网卡会连接到网桥上，而Docker下，网桥和eth0网卡通过协议栈通信，这导致虚拟机的数据包通过网桥发往外网时，不需要做NAT，而容器的包需要做NAT，虚拟机的包不需要经过协议栈，而容器的包需要经过主机协议栈，也需要主机开启ip_forward功能，不然外界来的包会被直接丢掉，性能上会稍差一点。[2]</p>
<p>虚拟机和Docker的网桥连接方式的区别：
<img src="https://cdn.magichc7.com/static/upload/20210609/upload_a8222a8c5922d8fc2b9213449608c3a0.png" alt="网桥模型.png"></p>
<p>对网桥模型更进一步的解释可以参考[3]。</p>
<p>Docker网桥模式在单机内的Docker容器之间沟通时是可用的，而且性能不差，走的都是二层协议（可以从图中看到并不需要经过主机协议栈）。如果你的Kubernetes集群只有一个节点的话，那么其实这就已经可以用于构建单节点Kubernetes了。之所以有些同学使用kubeadm默认安装会失败，是因为没有配置好cidr地址池，需要把pod的cidr地址池与Docker的地址池配置成一样的。但是，对于多节点Kubernetes集群，这就不够用了。每个机器上的Docker0网桥是不互通的，pod的地址也会冲突。因此，就需要新的地址分配与通信策略，这些就是CNI要完成的工作。你可能会想，我把多个节点的Docker0网桥打平，让他们之间能互通，并且可以不冲突的分配地址不就好了。没错，这也是一种CNI的实现思路。我们来看看别人是怎么做的。</p>
<h1><a id="toc-525" class="anchor" href="#toc-525"></a>二层网络和三层网络</h1>
<p>在正式介绍不同的CNI实现之前，需要先看一下，什么是二层网络，什么是三层网络。不同的CNI在大的拓扑上有不同的实现方式，像Flannel就是个二层方案，Calico是三层方案，Cilium可以做二层也可以做三层。</p>
<p>二层、三层是按照逻辑拓扑结构进行的分类，并不是说ISO七层模型中的数据链路层和网络层，而是指核心层，汇聚层和接入层，这三层都部署的就是三层网络结构，二层网络结构没有汇聚层。[4] 二层网络和三层网络一个明显的区别就是寻址方式的区别。二层网络没有IP地址和路由的概念，网络基于MAC地址寻址转发数据，数据以帧格式存在，不容易控制，存在广播风暴，一个二层网络是一个广播域一个冲突域。L2=物理层＋数据链路层，属交换网络（MAC地址识别） 。L3包含了IP地址概念，可以路由，网络基于IP地址寻址转发，数据以包的形式存在，容易控制，隔离广播风暴，L3层网络的一个子网是一个广播域一个冲突域。[5]更多的细节内容可以看看引文的[4]和[6]。</p>
<h1><a id="flannel" class="anchor" href="#flannel"></a>Flannel</h1>
<p>Flannel是一种基于二层Overlay网络的CNI实现方式。（什么是Overlay网络参见引文[7]）简单来说就是，Flannel将不同主机之间的包通过其维护的二层隧道直接转发。Flannel解决了这样两个问题：</p>
<ul>
<li>不同机器上Pod的网段划分的问题</li>
<li>不同机器上Pod的寻址和通信问题</li>
</ul>
<p>它并没有在这之外做其他的工作，没有提供限流等等的功能。</p>
<blockquote>
<p>Flannel is focused on networking. For network policy, other projects such as Calico can be used. <a href="https://github.com/flannel-io/flannel">Github: Flannel</a></p>
</blockquote>
<p>下图是使用了VXLAN Backend的Flannel的结构图。
<img src="https://cdn.magichc7.com/static/upload/20210609/upload_e8eb3931f3a1525bfb9ab42542b04a2d.png" alt="image.png"></p>
<p>Flannel在节点上会安装两个设备，分别是cni0网桥设备和flanne1.1隧道设备。其中，cni0对应于图中的docker0。如果主机使用的容器引擎是Docker，那么主机上还会有一个Docker0网桥，他们的作用是一样的。Docker的网桥模式把容器的veth设备对的对端接在Docker0网桥上，Flannel把它接在cni0网桥上。之所以需要创建一个新的网桥，是因为一般默认的Docker0网桥与Flannel的默认地址池不一样。Flannel.1是一个隧道设备。当内核根据路由表将</p>
<p>我们可以看一个实际部署了Flannel的机器上的路由表信息，本机容器在网段中被分配的地址是10.244.0.0/24，另外两个机器上10.244.1.0/24和10.244.2.0/24。主机的外网地址分别是192.168.111.25，192.168.111.26和192.168.111.27。
25机器上的网卡信息：
<img src="https://cdn.magichc7.com/static/upload/20210609/upload_c3ad3a671e4a8afa66c7929c5eded6e7.png" alt="image.png">
<img src="https://cdn.magichc7.com/static/upload/20210609/upload_10f50e8eef026ac879d56963592712f6.png" alt="image.png"></p>
<p>25机器上的路由表信息：
<img src="https://cdn.magichc7.com/static/upload/20210609/upload_03a999400caa453cea3e12f670c6ad31.png" alt="image.png"></p>
<p>我们可以看到，25上，所有目标为10.244.0.0/24（本机容器）的包都会发往cni0网桥设备，本机通信，发往10.244.1.0/24和10.244.2.0/24（其他机器上的容器）的包都会被发往Flannel.1隧道设备。</p>
<p>Flannel.1为vxlan设备，当数据包来到flannel.1时，需要将数据包封装起来，因为是通过二层协议寻址，就需要目标ip地址对应的mac地址。此时，flannel.1不会发送arp请求去容器的mac地址，而是由Linux kernel将一个“L3 Miss”事件请求发送的用户空间的flanned程序。Flanned程序收到内核的请求事件之后，从etcd查找能够匹配该地址的子网的flannel.1设备的mac地址，即目标pod所在host中flannel.1设备的mac地址。Flannel在为Node节点分配ip网段时记录了所有的网段和mac等信息，并存储在ETCD中。[8]</p>
<p>同样的，在其他机器上，同机容器的包直接通过网桥转发，跨节点访问走隧道。这是26节点的路由表。</p>
<p><img src="https://cdn.magichc7.com/static/upload/20210609/upload_b87e49af6cb2a0067dba17fe50e25ba4.png" alt="image.png"></p>
<p>因为实际上采用的是二层网络，因此Flannel组网有很大的局限性。</p>
<ul>
<li>子网的规模不会很大</li>
<li>跨节点访问过程中，因为要走隧道，需要重新封一个二层数据帧和解二层数据帧，因此性能有损失。</li>
</ul>
<p>出于性能的考虑的话，Flannel并不是一个很好的选择，因此才有了其他的网络通信方案。</p>
<h1><a id="toc-3b6" class="anchor" href="#toc-3b6"></a>引用</h1>
<ul>
<li>[1] guotianqing, <a href="https://blog.csdn.net/guotianqing/article/details/82356096">linux中的网络命名空间的使用</a>, CSDN</li>
<li>[2] 猿大白, <a href="https://www.cnblogs.com/bakari/p/10529575.html">Linux 虚拟网络设备详解之 Bridge 网桥</a>, 猿大白@公众号「Linux云计算网络」</li>
<li>[3] 
public0821, <a href="https://segmentfault.com/a/1190000009491002">Linux虚拟网络设备之bridge(桥)</a>, Segmentfault</li>
<li>[4]工业通信发烧友, <a href="https://www.zhihu.com/search?type=content&amp;q=%E4%BA%8C%E5%B1%82%E7%BD%91%E7%BB%9C%E5%92%8C%E4%B8%89%E5%B1%82%E7%BD%91%E7%BB%9C%E7%9A%84%E5%8C%BA%E5%88%AB">二层网络结构和三层网络结构的对比</a>, 知乎</li>
<li>[5] luuJa_IQ, <a href="https://blog.csdn.net/luuJa_IQ/article/details/104198784">L2\L3层网络区别</a>, CSDN</li>
<li>[6]  -零, <a href="https://www.cnblogs.com/-wenli/p/9630261.html">网络二层,三层的区别和寻址过程</a>, 博客园</li>
<li>[7] draveness.me, <a href="https://draveness.me/whys-the-design-overlay-network/">为什么集群需要 Overlay 网络</a>, draveness.me</li>
<li>[8] 金色旭光, <a href="https://www.cnblogs.com/goldsunshine/p/10740928.html">k8s网络之Flannel网络</a>, 金色旭光</li>
</ul>

            ]]></description>
            <pubDate>Wed, 09 Jun 2021 08:00:00 GMT</pubDate>
            <guid>https://blog.magichc7.com/post/Kubernetes-cni-series-1.html</guid>
        </item>
    </channel>
</rss>
