bash 里 I have no name!

通常情况下, 我们启动 Docker 都是以 root 为当前用户启动 container 的. 但是我们也可以设置以特定的用户来运行 container 里面的主进程.

  1. 加上 --user 参数

    # 这里指定一个不存在的 user id 1000 来执行. 
    docker run --name noNameTest --user 1000:1000 neo4j:latest
  2. 在 Dockerfile 里面设置用户

    USER <user>[:<group>] # 用户名:组名
    USER UID[:GID] # 用户ID:组ID

但是如果你登录进入该 contianer 却看到你的用户名是: I have no name!, 那说明你的 Shell (Bash 或其它) 再向你抱怨, 它无法找到你当前的用户名. 如下面:

I have no name!@my_container:$ 

I have no name!@my_container:$ id
uid=1000 gid=1000 groups=1000
I have no name!@my_container:$ whoami
whoami: cannot find name for user ID 1000

这是因为 Shell 通常去 /etc/passwd 去找这个用户ID, 却找不到.

在 Kubernetes 里面这样以非 root 用户去运行, 可以最小化权限, 防止特权升级, 提高安全性和隔离性.当容器以不存在的用户 ID 和组 ID 运行时,通常这些用户没有在容器内配置 sudo 或 su 的权限,也没有相应的配置文件(如 /etc/sudoers)来允许权限提升.

在 Dockerfile 中使用 USER 指令指定非 root 用户。
在 Kubernetes Pod 的安全上下文(SecurityContext)中指定 runAsUser 和 runAsGroup 来配置非 root 用户运行。

http status code 431

这周末处理一个 http status code 431 的 case. 431 表示 header 字段的数据太大, 听起来也没那么复杂, 就是消减一下 header 内容就解决了. 但是在大家都说最近没有任何改动的情况下, 到底哪里出问题了呢?

431 表示什么?

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431 介绍看. 有 2 种可能的情况:

  1. 所有 header 的字段数据加起来太大了;
  2. 单个 header 的字段数据太大了;

谁会报这个错?

听起来这个问题有点多余, 当然是应用服务器了. 其实不然. 中间可以拆包的所有网络组件都有可能, 比如: 可以拆包的代理, 负载均衡服务器, 如今应用广泛的反向代理服务器 envoy, nginx 等. 他们都可以拆开 http 的报, 检查一下 header 的大小, 然后报个 431 给客户端.

例子

下面便是当时的一个例子, 把其中一个字段加大到60多KB. 可以看到 header 是431, response payload 是一句话.
这个例子是 envoy 拆包后发现 header 很大, 根据 envoy 官方文档: max_request_headers_kb, 它不会验证单个 header 大小, 而是验证整个header 大小. 默认是60K, 所以这里超了.
header.png

网络链路添加 header

通常情况下, 做为客户端发出的所有 header 就是服务器端收到的所有 header, 但是这些中间会拆包的各个组件其实都有可能添加新的 header 到原始的数据报文中. 比如: 一般负载均衡服务器会添加一个 header 告诉下游服务器真正的客户端的 IP 是多少, 这样下游服务器就知道真正的客户IP 是多少了? 否则它通过 remote IP 获得的就是负载均衡服务器的IP.

有时候, 中间拆包的组件可能会添加更多的 header 用来输送更多的信息给下游, 比如: 当前拆包的组件名称, 它的IP, 它的版本之类的.

如何区分到底是最终应用服务器返回431 还是中间拆包组件?

通常情况下, 这些不同的组件或者应用服务器返回的 response header 里面会标明它的身份. 比如我们的例子中它就是中间的 envoy 组件返回的, 不是最终的应用服务器. 应用服务器会返回带不同 header response.

为什么 header 会很大?

各种原因都会有, 比如:

  1. Referer 是一个巨长的 URL.
  2. Cookie 的内容太多 (Cookie 的内容都是header里面的)
  3. 某个 header 添加了巨多内容.

具体在我们这个case 里面, 就是客户端有个小系统, 它为了不让下游子系统去到一个远程系统取很多数据, 直接把这部份内容放到 header 发给下游了, 然而这部份内容是一个动态变动的部分, 就像一个list, 会随着系统增加减小, 正巧它这次很大, 就遇到这种情况.

汇编指令把数据从内存一块区域复制到另外一块区域

最近再次阅读 CSAPP 里面的有关汇编的一节, 它说到了 mov 指令, mov 可以把常量, 寄存器的值, 内存的值移动(move)到寄存器, 内存. 但是不能直接把某个地址内存的值移动到另外一个内存地址. 为什么会有这个限制?

如果考虑到当前的值要做计算, 那么这个值从内存加载到CPU, 然后做计算, 之后再保存到内存. 看上去没啥性能损失, 但是假如只是把某个内存的数据复制(移动)到另外一个内存地址, 那么这个性能损失就很大.

为什么会这么想, 这是因为在 这本书里提到这么一个有关延迟度量的对比:
latency scale.png

可以看到从内存加载, 然后再保存到内存需要的相对时间确实很长, 如果有一个方案直接告诉内存控制器, 把某地址开始的多少数据直接复制到另外一块区域, 那将是飞速提升, 因为只要CPU发出一个内存控制器的指令就可以. 但是这看上去很美, 因为内存控制器并不知道进程虚拟内存这些东西, 它只知道硬件物理内存. 虚拟内存要写的2个连续字节可能在物理内存上是相隔很远的两块地方.

如果一个内存数据并不需要做算术逻辑运算, 那么可以使用专门的字符处理指令 movs, 它有对应不同宽度的: movsb, movsw, movsd, movsq. 它们的使用会涉及到 RSI, RDI, RCX 寄存器, 以及 DF flag, 并且结合 REP 指令来实现连续复制.

但是这里我更关心的是, 这些被移动的字节是不是要到CPU绕一圈? 也就是说这些字节是不是需要占用系统总线(system bus). 根据我互联网上查到的数据, 这些字节通常情况下都需要到 CPU 绕一圈, 涉及到加载(load)和保存(save). 也就是避免不了性能的延迟. 但是如果要移动的数据已经在缓存 (L1, L2, L3), 那么将不需要走系统总线加载. 同时这些加载可能都是成块加载, 所以通常连续的内存区域都会同时被一次加载, 提高了性能. 另外从缓存刷新到内存也是成块(页)刷新到, 那么它也会提高性能.

go语言把部分CPU从 AVX 迁移到 REP movsb - https://github.com/golang/go/issues/66958

有关 memcpy

Linux内核中的memcpy函数是一个用于内存复制的函数,它的主要功能是将一块内存中的数据原封不动地复制到另一块内存中。这个函数在内核编程中非常重要,因为它直接操作内存,效率要求非常高。

声明在 string.h 中
void *memcpy(void *dest, const void *src, size_t n);

为了提高效率,Linux内核中的memcpy通常实现为汇编语言,特别是对于小数据块的复制,会有特别的优化。对于更大的数据块,它可能会利用CPU的特定指令(如SSE/AVX指令集)来提高复制速度。

memcpy 可以根据CPU 的指令集选用 movs 系列的指令, 也可以选择 AVX 或 SIMD 来提高在该架构上的性能.

summary

从内存到内存的复制, 还是需要占用系统总线的.

Linux source 命令是 bash 的 builtin

今天执行一个 shell 脚本, 发现 shell 里面报错: source: not found. 可是去执行 source 命令, 发现又有. 到底怎么回事呢?

原来 source 是 bash 的 builtin 命令, 而我运行这个脚本使用的是 sh script.sh, 它其实使用的是 /usr/bin/sh, 然而在这个 Ubuntu 上面 /usr/bin/sh 其实是指向了 /usr/bin/dash. 然而 source 这个命令在 dash 没有.

supra@suprabox:~$ which sh
/usr/bin/sh
supra@suprabox:~$ which bash
/usr/bin/bash

# 没有找到 source 命令
supra@suprabox:~$ which source

# source 的类型
supra@suprabox:~$ type source
source is a shell builtin

# source 命令存在, 当前我的默认shell 是bash
supra@suprabox:~$ source -h
-bash: source: -h: invalid option
source: usage: source filename [arguments]

supra@suprabox:~$ ls -lah /usr/bin/sh
lrwxrwxrwx 1 root root 4 Mar 31  2024 /usr/bin/sh -> dash
supra@suprabox:~$ ls -lah /usr/bin/bash
-rwxr-xr-x 1 root root 1.4M Mar 31  2024 /usr/bin/bash
supra@suprabox:~$ ls -lah /usr/bin/dash
-rwxr-xr-x 1 root root 127K Mar 31  2024 /usr/bin/dash

Bash 内置的命令执行不会启动另外一个进程, 在当前进程执行.

enable -a 启用所有内置命令, 即查看所有内置命令.

supra@suprabox:~$ enable -a
enable .
enable :
enable [
enable alias
enable bg
enable bind
enable break
enable builtin
enable caller
enable cd
enable command
enable compgen
enable complete
enable compopt
enable continue
enable declare
enable dirs
enable disown
enable echo
enable enable
enable eval
enable exec
enable exit
enable export
enable false
enable fc
enable fg
enable getopts
enable hash
enable help
enable history
enable jobs
enable kill
enable let
enable local
enable logout
enable mapfile
enable popd
enable printf
enable pushd
enable pwd
enable read
enable readarray
enable readonly
enable return
enable set
enable shift
enable shopt
enable source
enable suspend
enable test
enable times
enable trap
enable true
enable type
enable typeset
enable ulimit
enable umask
enable unalias
enable unset
enable wait

git 本地 branch 按照使用时间排序

使用 git 做开发, 我们的 branch 策略是每个新功能, 每个要修改的bug 都在一个单独的 branch 上坐开发. 久而久之, 本地就有很多 branch 了, 有些还没有合并到 master 分支上, 有些是正在开发的本地 branch.

如何查看最近使用的 branch?

# 查看最近使用的 branch 时间先后排序
git branch -l --sort committerdate

# 查看最近使用的 branch 时间倒序排序
git branch -l --sort -committerdate

# 查看最近使用的 branch 时间倒序排序
git for-each-ref --sort=-committerdate refs/heads/

# 查看最近使用的 branch 时间倒序排序 特定格式
git for-each-ref --sort=-committerdate refs/heads/ --format='%(committerdate:iso8601) %(refname:short)'