跳转至

Welcome

搭建k8s集群

引用 https://blog.piaoruiqing.com/2019/09/17/kubernetes-1-installation/ 照着文章搭建k8s集群,写得挺好,记录一下自己的搭建方法和问题

环境

  1. 2个kvm虚拟机, 安装的centos7系统
  2. 两个虚拟机都配置了双网卡,eth0和eth1, eth0桥接宿主机的bridge, eth1 接入libvirt default NAT网络
  3. 官方推荐配置: CPU > 2,内存 > 2G
  4. eth1 的ip 分别为 192.168.122.61, 192.168.122.161

Master和Worker 共同步骤

需要在所有节点执行

修改hostname

编辑hostname

vim /etc/hostname # 修改hostname
配置host文件, 将所有节点都指定host

关闭防火墙

  1. systemctl disable --now firewalld
  2. 修改 /etc/seconfig, 将selinux 配置成disable

关闭 swap 分区

  1. swapoff -a
  2. 修改 /etc/fstab, 注释掉swap 记录

安装docker 配置镜像源

修改 /etc/docker/daemon.json 文件

{
  "registry-mirrors": ["https://xxxxxxxx.mirror.aliyuncs.com"],
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}

### 添加k8s 软件源

如下添加的aliyun的镜像源

``` cat < /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64 enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg exclude=kube* EOF

### 安装k8s

安装组件 
yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes systemctl enable kubelet && systemctl start kubelet
### 配置网络
cat < /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF sysctl --system
上面的步骤需要在主备上执行

## Master 操作 

下面的步骤只在Master节点运行

### 生成k8s初始化配置文件 
[root@k8s-master ~]$ kubeadm config print init-defaults > kubeadm-init.yaml
得到 kubeadm-init.yaml 文件

修改kubeadm-init.yaml 文件
1. 修改 `advertiseAddress: 1.2.3.4` 为本机地址 192.168.122.61
2. 修改 `imageRepository: k8s.gcr.io ` 为 imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers 

修改镜像地址,这样就避免下载k8s 镜像超时

修改后的kubeadm-init.yaml

```yaml
apiVersion: kubeadm.k8s.io/v1beta2
bootstrapTokens:
- groups:
  - system:bootstrappers:kubeadm:default-node-token
  token: abcdef.0123456789abcdef
  ttl: 24h0m0s
  usages:
  - signing
  - authentication
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 10.33.30.92
  bindPort: 6443
nodeRegistration:
  criSocket: /var/run/dockershim.sock
  name: k8s-master
  taints:
  - effect: NoSchedule
    key: node-role.kubernetes.io/master
---
apiServer:
  timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta2
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns:
  type: CoreDNS
etcd:
  local:
    dataDir: /var/lib/etcd
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers
kind: ClusterConfiguration
kubernetesVersion: v1.15.0
networking:
  dnsDomain: cluster.local
  serviceSubnet: 10.96.0.0/12
scheduler: {}

下载k8s的组件镜像

kubeadm config images pull --config kubeadm-init.yaml

初始化 k8s

kubeadm init --config kubeadm-init.yaml
执行无误后,得到worker 节点加入集群的命令
...
Your Kubernetes control-plane has initialized successfully!
...
Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.33.30.92:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:2883b1961db36593fb67ab5cd024f451b934fc0e72e2fa3858dda3ad3b225837 

记录这个命令, 丢失的话也可以重新执行 kubeadmin create token 找回

配置普通用户执行kubectl 命令

用普通用户执行一下命令, 这样普通用户也能管理集群

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

查看Master 节点状态

可见,Master节点未就绪,这是因为还没安装网络组件

[root@k8s-master kubernetes]$ kubectl get node
NAME         STATUS     ROLES    AGE     VERSION
k8s-master   NotReady   master   3m25s   v1.15.3

配置网络

  1. 下载calico 描述文件 wget https://docs.projectcalico.org/v3.8/manifests/calico.yaml

  2. 修改calico 描述文件里的serviceSubnet,修改为跟kubeadmin-init.yaml一致

    cat kubeadm-init.yaml | grep serviceSubnet:
    serviceSubnet: 10.96.0.0/12
    

注意
1. calico 的地址必须和 kubeadm-init.yaml 保持一致, kubeadm-init.yaml 默认为 serviceSubnet: 10.96.0.0/12, 而calico的默认地址为192.168.0.0/16, 所以要么修改kubeadmin-init.yaml, 要么修改calico.yaml。 2. 这里的Subnet 不能涵盖到主机的地址范围即不能包含 192.168.122.0/24

安装calico

kubectl apply -f calico.yaml 

查看Master节点

当calico 安装好了, 可以发现Master 节点变为就绪, 至此Master 节点就配置好了

[root@k8s-master ~]$ kubectl get node
NAME         STATUS   ROLES    AGE   VERSION
k8s-master   Ready    master   15m   v1.15.3

安装dashboard

之前的步骤安装好了Master节点, 可选择安装dashboard,通过网页来管理集群

步骤很简单, 先下载dashboard, 再安装

[root@k8s-master ~]$ wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml
[root@k8s-master ~]$ kubectl apply -f recommended.yaml 

创建dashboard 用户

必须创建用户,然后获取token,才能在集群外访问, 否则必须在Master 的localhost 访问dashboard。 以下是创建用户的描述文件, 注意namespace, 例如官方的例子是 dashboard, 这样生成的token 不一样

执行安装用户 kubectl apply -f dashboard-adminuser.yaml

# dashboard-adminuser.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kube-system

创建证书, k8s 不允许远程使用http,所以需要用证书访问dashboard

[root@k8s-master ~]$ grep 'client-certificate-data' ~/.kube/config | head -n 1 | awk '{print $2}' | base64 -d >> kubecfg.crt
[root@k8s-master ~]$ grep 'client-key-data' ~/.kube/config | head -n 1 | awk '{print $2}' | base64 -d >> kubecfg.key
[root@k8s-master ~]$ openssl pkcs12 -export -clcerts -inkey kubecfg.key -in kubecfg.crt -out kubecfg.p12 -name "kubernetes-client"

将生成的 kubecfg.p12 文件导入到集群外的主机,即kvm宿主机上

scp root@10.33.30.92:/root/.kube/kubecfg.p12 ./

在宿主机上使用chrome 的高级功能里可以导入证书

重启chrome 登录后登录

https://192.168.122.61:6443/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/login

打开网页后选择输入token, token通过在Master 节点执行一下命令生成, 注意参数 -n kube-system, 需要跟创建用户时的namespace保持一致

kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')

复制token 到网页上,就能登录Dashboard

Worker节点加入集群

直接在worker节点执行

kubeadm join 10.33.30.92:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:2883b1961db36593fb67ab5cd024f451b934fc0e72e2fa3858dda3ad3b225837

然后到Master 节点执行以下命令, 这里Name 显示了k8s-worker是因为我们在Hosts文件里指定了ip,所以k8s自动识别到了,否则会显示节点ip

[root@k8s-master ~]$ kubectl get node
NAME         STATUS   ROLES    AGE   VERSION
k8s-master   Ready    master   10h   v1.15.3
k8s-worker   Ready    <none>   96s   v1.15.3

至此集群搭建完毕

遇到问题

  1. 因为我的虚拟机配置的双网卡, 需要在calico.yaml 配网卡interface
    # Cluster type to identify the deployment type
                - name: CLUSTER_TYPE
                  value: "k8s,bgp"
                - name: IP_AUTODETECTION_METHOD
                  value: "interface=eth1"
    

decoder 介绍

python decoder(装饰器)是python的高级用法,用来拓展函数或类的功能。不过也增加初学者 阅读代码的难度

例如flask可以在需要验证用户登录的请求函数上添加装饰器,只有当用户登录了才可以调用该>页面,非常方便

原理

decoder 其实也是一个函数,装饰器函数会返回真实函数,并会增加额外的代码,如

# 定义wraper
def log(func):
    def wrapper(*arg):
        print('func called')
        return func(*arg)

    return wrapper

# 使用
def hello(Name=None):
    print('Hello %s'%Name)

new_hello = log(hello)

new_hello('bob')
输出 func called Hello bob

使用wraper 的原因

提供额外的功能,例如计时,日志等。 另外wraper还有更多强大的功能

def fib(n):
    if n <= 1: return 1
    return fib(n-1) + fib(n-2)

这种递归调用非常费时,而使用wraper 可以提供一个缓存器

def memory(func):
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memory
def fib(n):
    if n <= 1: return 1
    return fib(n-1) + fib(n-2)


## 给类wraper

上面是函数wraper 类也可以作为wraper 但需要类支持()调用类似c++的类函数
在python里需要实现 `__call__` 方法

```python
import functools

class MemoryClass():
    def __init__(self, is_logging, func):
        self.is_logging = is_logging
        self.func = func
        if self.is_logging:
            print('logging')
        self.cache = {}

    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)

        return self.cache[args]

def memory(is_logging):
    return functools.partitial(MemoryClass, is_logging)


@memory(is_loging=True)
def fib(n):
    if n <= 1: return 1
    return fib(n-1) + fib(n-2)


print(fib(5))
类wrapr 可以增加参数,相比函数装饰器,类装饰器具有更加灵活,封装性更好等优点~

wraper 带来的问题

  1. 改变原来的函数属性,例如函数名, 因为装饰器用新函数替换了原来的旧函数。 所以新函>数少了旧函数的属性 解决办法
import functools

def advertising(func):
    @functools.wraps(func)
    def wrapper(*args):
        print('欢迎关注微信公众号: Charles的皮卡丘')
        return func(*args)
    return wrapper

@advertising
def add_1(a, b):
    '''加法运算'''
    return a + b
  1. 获取参数
import inspect
import functools

def check(func):
    a = 0
    @functools.wraps(func)
    def wraper(*args, **kwargs):
        a += 1
        print(a)
        # 在wraper里检查参数
        getcallargs = inspect.getcallargs(func, *args, **kwargs)
        if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:
            return False
        return func(*args, **kwargs)

    return wraper


@check
def dowork(arg1, arg2):
    return arg1 + arg2

dowork(1,2)

此时会报错,a在赋值之前引用了。 需要将a声明为 nonlocal

区别

Dockerfile 中,ENTRYPOINT和CMD来都可以用来设置默认命令,而代替docker run时的命令,也可以一起使用

单独使用ENTRYPOINT或CMD时,其区别在于对docker run的参数处理:
CMD的命令会被docker run的参数覆盖,而ENTRYPOINT是追加

例如:

# image1
FROM ubuntu
ENTRYPOINT ["echo", "hello world"]
# image2
FROM ubuntu
CMD ["echo", "hello world"]

执行结果

# docker run image1
hello world

# docker run image1 hostname
0c33ff9af81c

# docker run image2
hello world

# docker run image2 hostname
hello world 0c33ff9af81c

实际情况多是entrypoint与cmd组合使用

cmd 为entrypoint 提供默认参数, 即如果'docker run'给了参数时,覆盖CMD参数,否则使用CMD提供的参数

FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

首先说cmd和entrypoint 都有两种模式

  1. shell模式, 即没有用数组来传递参数, 直接接命令如 CMD top -p 或者 entrypoint top -p
  2. exec 模式, 即使用数组表示如 CMD ["top", "-p"], 注意必须用双引号,因为这个是用json来传递的

两者区别:
shell模式不会接受命令行参数,而且1号进程是sh,而非如上的top。 这样的话top不能接受到docker stop container时的信号。 而一般我们写的程序都是捕捉SIGTERM来进行peaceful exit,那么就一定不能使用shell模式

exec模式时, 可以接受docker run image param, 这个param会传递给top作为参数。 而且top 是1号进程,能接收信号。

细微区别: 由于shell模式是先启动sh, 符号解析由shell执行,例如$HOME的展开由shell进行, 而用exec模式时,由docker 进行

总之,推荐使用exec 模式

额外话, 减小镜像体积

使用'\' 或者 '&&' 将命令连起来可以减小镜像层数,从而减小体积。另外build时,单独一行的命令会在新的layer 层执行。有些命令产生的cache,会持续影响到下一层
例如RUN apt-get dist-upgrade -y,如果不想保留cache,需要在build时加入参数docker build --no-cache.

还有最有效减小镜像体积的方式,也常见的是最后用空白镜像,'FROM SCRATCH', 这样把其他的层都干掉了, 只保留一层镜像

问题, 如果在文件系统之上再创一个文件系统?

例如在ext3的文件系统上创建一个xfs的文件系统,可以通过回环设备loop, 我们经常通过 mount -o loop 来 mount一个iso文件 但mount 的选项总是ro的

mount: /mnt: WARNING: device write-protected, mounted read-only.

不仅如此, 先在当前文件系统dd出一个文件, 再绑定到loop设备上,然后mount 到某个目录后, 可以进行读写访问

[root@ha1 ~]# dd if=/dev/urandom of=file bs=1M count=2
2+0 records in
2+0 records out
2097152 bytes (2.1 MB, 2.0 MiB) copied, 0.0164121 s, 128 MB/s

[root@ha1 ~]# mkfs.ext3  file 
mke2fs 1.44.6 (5-Mar-2019)
Discarding device blocks: done                            
Creating filesystem with 2048 1k blocks and 256 inodes

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

[root@ha1 ~]# mount -o loop file  /mnt
[root@ha1 ~]# ls /mnt/
lost+found

[root@ha1 mnt]# echo abc > abc
[root@ha1 mnt]# ls
abc  lost+found
绑定loop设备和挂载是由mount 一个命令完成的。

手动绑定loop

[root@ha1 ~]# losetup -f
/dev/loop1

[root@ha1 ~]# losetup -f file 
这样只是将文件绑定了loop设备,需要再挂载到文件目录mount /dev/loop0 /mnt

[root@ha1 ~]# ls /mnt/
abc  lost+found
[root@ha1 ~]# 

losetup -f 可以返回第一个未被使用的loop设备名

创建loop设备

有的系统默认创建了 loop0 .. loop7 的块设备,有的则是在需要的时候创建,比如mount iso的时候发现没有loop设备,则会创建

  1. 手动创建loop设备通过 mknode 创建
mknode  /dev/loop0 b 7 0
mknode  /dev/loop1 b 7 0
...

mknode /dev/loop7 b 7 0
  1. 如果8个loop0 .. loop8 设备都占用了, 可以再创建loop8

$sudo mknod /dev/loop8 b 7 8 

$ls -l /dev/loop8
brw-r--r-- 1 root root 7, 8 Jun 11 19:16 /dev/loop8
ps: /dev/loop* 是块设备

bash 脚本安全

不管是大厂还是小厂,总能听到一些误操作新闻
那避免误操作就办法就是写脚本, 然后也有很多脚本出事的新闻, 通常有以下

引用了不存在的变量

#!/bin/bash

ABC_PATH=abc

/bin/rm ${ABC_PATH}/

如果ABC_PATH变量被unset了, 那么就是删除系统了
不过听说/bin/rm / 的操作会可能被系统禁止, 但是/bin/rm /*则肯定能执行成功

脚本没有输出

有很多公司用堡垒机来审计操作记录, 但是如果脚本没有输出,那么就不知道执行了什么命令, 再如果脚本也被删除死无对证,那就无法分析了

解决办法

在bash 脚本前面加上 set -xeu

-x 调试模式,bash会输出所有命令 -e 遇到错误就退出 -u 引用了不存在的变量就退出

添加默认值

path=${ABC_PATH-/} #default /

这样,当ABC_PATH不存在时,就使用默认值'/'

最好的办法

使用ansible, 可以对脚本进行版本管理和review, 可以避免脚本难以维护的问题 使用ide + bash 插件, 可以有提示

良好的脚本习惯

除了在脚本前面加上 set -xeu外, 有一些建议

  1. 多用&&,|| ,少用 ; ,如果前面的命令出错,后面就不会执行, 而; 不管
  2. 善用 date, 例如将日志文件加上日期 bash test.sh > log_date +%m_%d_%H_%M_%S``
  3. 脚本放在指定目录,不要与工作目录放在一起, 避免脚本自己被删, 死无对证
  4. 定期备份

bash 的条件判断

有以下脚本,当TMP 不为空的时候,才进行判断值是否大于2

if [ -n "$TMP" -a ${TMP} -gt 2 ];then
        echo "aaaa"
else
        echo "bbbbbbbb"
fi

而当TMP 为空的时候,就会出现 too many arguement。这就说明了bash的执行流程,是先进行整行替换再执行(起码是整行,也许还是全文替换)。 替换后[-n "" -a -gt 2]。导致了报错。 解决这样的问题可以用默认值 TMP=${TMP:=2}, 这样当TMP为空的时候就会自动赋值为2

if [ -n "$TMP" -a ${TMP:=2} -gt 2 ];then
        echo "aaaa"
else
        echo "bbbbbbbb"
fi

register 关键字

register 关键字是c/c++ 里面唯一能操作寄存器的指令,强制将数据存放在寄存器里,而不是放在堆栈里,这样读写速度最快。

而汇编语言强大之处是能直接操作寄存器,且可以指定使用哪些寄存器。

通过汇编一段代码来演示register的作用

先是c语言代码

int main(){

    register int i;

    for(i = 0 ;i<10;i++){
        int j = i;
    }

}
对应的汇编
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        pushq   %rbx
        .cfi_offset 3, -24
        movl    $0, %ebx
        jmp     .L2
.L3:
        movl    %ebx, -12(%rbp)
        addl    $1, %ebx
.L2:
        cmpl    $9, %ebx
        jle     .L3
        movl    $0, %eax
        movq    -8(%rbp), %rbx
        leave
        .cfi_def_cfa 7, 8
        ret

没有register关键字的时候

int main(){

    int i;

    for(i = 0 ;i<10;i++){
        int j = i;
    }

}

main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $0, -8(%rbp)
    jmp .L2
.L3:
    movl    -8(%rbp), %eax
    movl    %eax, -4(%rbp)
    addl    $1, -8(%rbp)
.L2:
    cmpl    $9, -8(%rbp)
    jle .L3
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret

可见,当register修饰int i时, i的值存放在%ebx里,当没有register修饰时,i的值放在栈里%rbp -8的位置 movl %ebx, -12(%rbp) 可见当register时, j的值复用了ebx的高32位。 此时的内存如下

--- 栈底
旧的rbp 64位
---
旧的rbx 高32位
---  j
旧的rbx 低32位
---栈顶

rbp表示基地址指针,指向的是函数栈的起始地址,这个起始地址只是对于当前函数而言的
rsp表示栈顶指针,指向当前的栈顶
每次函数调用时(main 函数被内核_start_main__调用): 1. 将调用者的函数栈基地址保存起来
2. 参数压栈, 可能由调用者压栈,可能由被调用者压栈,影响的是当前的栈顶,继而栈顶影响rbp 3. 将被调用函数当前的栈顶复制给基地址寄存器。新的rbp == rsp即当前的栈顶。相当于构造了新的函数运行环境

rbp和rsp

实例代码,main函数中调用foo函数。

int foo(){
  printf("hello");
  return 1;
}

int main(){
  foo();
  return 0;
}

(gdb) disassemble 
Dump of assembler code for function main:
   0x0000555555555158 <+0>: push   %rbp
   0x0000555555555159 <+1>: mov    %rsp,%rbp
   0x000055555555515c <+4>: mov    $0x0,%eax
=> 0x0000555555555161 <+9>: call   0x555555555139 <foo>
   0x0000555555555166 <+14>:    mov    $0x0,%eax
   0x000055555555516b <+19>:    pop    %rbp
   0x000055555555516c <+20>:    ret
  1. main 函数> push %rbp 将栈底寄存器的值保存到堆栈中
  2. main 函数> mov %rsp,%rbp 将当前的栈顶寄存器的值复制到栈底寄存器
  3. main 函数> mov $0x0,%eax 将foo函数的返回值初始化成0

进入到foo函数中

(gdb) disassemble 
Dump of assembler code for function foo:
   0x0000555555555139 <+0>: push   %rbp
   0x000055555555513a <+1>: mov    %rsp,%rbp
=> 0x000055555555513d <+4>: lea    0xec0(%rip),%rax        # 0x555555556004
   0x0000555555555144 <+11>:    mov    %rax,%rdi
   0x0000555555555147 <+14>:    mov    $0x0,%eax
   0x000055555555514c <+19>:    call   0x555555555030 <printf@plt>
   0x0000555555555151 <+24>:    mov    $0x1,%eax
   0x0000555555555156 <+29>:    pop    %rbp
   0x0000555555555157 <+30>:    ret
End of assembler dump.

附录

General-Purpose Registers

The 64-bit versions of the 'original' x86 registers are named:

    rax - register a extended
    rbx - register b extended
    rcx - register c extended
    rdx - register d extended
    rbp - register base pointer (start of stack)
    rsp - register stack pointer (current location in stack, growing downwards)
    rsi - register source index (source for data copies)
    rdi - register destination index (destination for data copies)

The registers added for 64-bit mode are named:

    r8 - register 8
    r9 - register 9
    r10 - register 10
    r11 - register 11
    r12 - register 12
    r13 - register 13
    r14 - register 14
    r15 - register 15

These may be accessed as:

    64-bit registers using the 'r' prefix: rax, r15
    32-bit registers using the 'e' prefix (original registers: e_x) or 'd' suffix (added registers: r__d): eax, r15d
    16-bit registers using no prefix (original registers: _x) or a 'w' suffix (added registers: r__w): ax, r15w
    8-bit registers using 'h' ("high byte" of 16 bits) suffix (original registers - bits 8-15: _h): ah, bh
    8-bit registers using 'l' ("low byte" of 16 bits) suffix (original registers - bits 0-7: _l) or 'b' suffix (added registers: r__b): al, bl, r15b

Usage during syscall/function call:

    First six arguments are in rdi, rsi, rdx, rcx, r8d, r9d; remaining arguments are on the stack.
    For syscalls, the syscall number is in rax.
    Return value is in rax.
    The called routine is expected to preserve rsp,rbp, rbx, r12, r13, r14, and r15 but may trample any other registers.

参考

https://shikaan.github.io/assembly/x86/guide/2024/09/08/x86-64-introduction-hello.html

乐观锁悲观锁

先说这两种锁都不是编程里面用到的锁, 而是一种策略。 在数据库中用的多。
而且在java里面有个原子bool AtomicBoolean 的实现也用到乐观锁的策略, AtomicBoolean::compareandswap

这里以数据库管理服务(DBMS)来说明这两种锁的区别,在并发的情况下

悲观锁

悲观锁策略预期是多个请求会操作同一个数据,导致数据不可靠。 悲观锁的策略就是上锁,不让其它请求修改, 即排他锁。

这种排他锁与我们平常编程用的互斥锁道理相同, 是性能比较低的一种锁,但能保证数据准确。

乐观锁

乐观锁实际是不上锁, 预期多个请求不会修改同一分数据。而是在最后提交的时候再去确认数据是否一致,如果一致则提交,如果不一致则报错,需要认为干涉

所以乐观锁的数据也是可靠的, 且性能较好。 这里关键是如何确认数据一致

乐观锁的确认并提交

这里举例sql的来说明

select quanity from items where id =1;

//得到quanity=3,然后更新

update items set quantity=2 where id=2 and quantity=3
更新时,会确认quantity为预期的3,否则不执行或者报错。

这样有个问题, 虽然单条语句是原子的,但是两条语句不是, 会出现ABA的情况

例如当一个请求1执行到select后,得知quantity=3, 然后请求2先更新quantity=2,然后又更新为quantity=3 然后请求1继续执行update,这会成功,虽然quantity=3是一致的,但是不能保证其它数据是一致的或者完全没有引起安全问题

因此另一种办法是判断只增version或者timestamp,每次请求时去更新。这两种办法都能确保数据一致。 但是因为数据库同时只允许一个请求修改version,那么就出现了竞争情况,会导致其它请求失败

例如多个请求同时记录了version=1, 则当一个请求完成的时候,version=2, 那么其它请求提交时发现verison变了,只能报告失败 虽然这样能保证数据正确,但是会导致其它请求失败

而电商平台中, 则采用另一种方式确认

select quanity from items where id =1;

//得到quanity=3,然后更新

update items set quantity=2 where id=2 and quantity > 0

这样就规避了上面两个方法的缺点

compareandswap

compareAndSet(expect_val, new_val)

上面的乐观锁就是compareandswap的解释:

先对比,如果一致则更新

再说一下java的实现,

    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))  
      UnsafeWrapper("Unsafe_CompareAndSwapInt");  
      oop p = JNIHandles::resolve(obj);  
      jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);  
      return (jint)(Atomic::cmpxchg(x, addr, e)) == e;  
    UNSAFE_END  

在unsafe.cpp 找到实现如下

#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:

int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
_emit 是用于生成指令的, 生成锁的指令,有cpu提供锁机制 从上下文看, 这个指令应该是上锁, 如果mp != 0 则上锁。

记录在部署一个nginx fastcgi 项目时失败, fastcgi 通过unix socket 通讯

这是一个flask 项目, 使用flup创建的fastcgi

#!/usr/bin/env python
from flup.server.fcgi import WSGIServer
from main import app

if __name__ == '__main__':
    # for apache
    # WSGIServer(app).run()
    # for nginx, need to set conmunication sock between nginx and the cgiserver
    WSGIServer(app, bindAddress='/tmp/myflask-fcgi.sock').run()

这里直接说明结果, socket 文件不能创建在/tmp/ 或者 /vat/tmp 目录。

nginx 错误配置

    location / { try_files $uri @myflask; }
    location @myflask {
        include fastcgi_params;
        fastcgi_param PATH_INFO $fastcgi_script_name;
        fastcgi_param SCRIPT_NAME "";
        fastcgi_pass unix:/tmp/fcgi.sock;
    }

nginx 错误提示

 connect() to unix:/tmp/fcgi.sock failed (2: No such file or directory) while connecting to upstream

原因

如果socket创建在/tmp 或者 /var/tmp 目录, 只能被socket的进程发现。 意思是只有cgi程序能发现,所以nginx 无法连接了

正确配置是创建到/var/sockets 目录,并注意权限问题

    location / { try_files $uri @myflask; }
    location @myflask {
        include fastcgi_params;
        fastcgi_param PATH_INFO $fastcgi_script_name;
        fastcgi_param SCRIPT_NAME "";
        fastcgi_pass unix:/var/sockets/myflask-fcgi.sock;
    }

关于unix socket

以上是一种解决办法,当然使用ip来连接nginx和cgi也是可以的,但是unix socket 有不少好处。

关于cgi

上面无论使用unix socket 还是 ip 来访问cgi, 这两种方式都是通过cgi来提供web服务。
而nginx 不支持cgi, 所以需要自己来通过python的 flup 来实现fastcgi。 nginx 只不过是一个代理的作用。而apache和tomcat 都能支持cgi

cgi 的定义是 common gateway interface, 是一个协议。 fastcgi,uwcgi都是升级版或者变种。  
flup实现了cgi的服务程序,也就是可以用来解析脚本(python php 等等),提供web服务。

另外除了以上方式, nginx也可以使用pypass proxy 来转发请求,由应用自己来实现网络处理。

location / {
        proxy_pass https://localhost:8080
        proxy_set_header Host            $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP       $remote_addr;
        proxy_set_header X-Scheme        $scheme;
        access_log /var/log/nginx/access.log my_tracking;
    }

如过是python 就需要监听某个端口,这种是不稳定的。 不适合生产模式下使用。

linux 添加打印机

记一次arch 上添加打印机的过程, 公司有一个Ricoh c5503 的打印机,ip 192.168.4.99 通过nmap这个地址可以发现开了不少端口

PORT     STATE SERVICE
21/tcp   open  ftp
23/tcp   open  telnet
80/tcp   open  http
139/tcp  open  netbios-ssn
514/tcp  open  shell
515/tcp  open  printer
631/tcp  open  ipp
7443/tcp open  oracleas-https
8080/tcp open  http-proxy
9100/tcp open  jetdirect

启动avahi-daemon 服务

这个服务提供mDNS功能, 可以发现局域网内的主机提供的服务,例如打印服务,ssh。 各个主机通过224.0.0.251 地址多播自己提供的服务
主机也可以配置自己想广告的服务,这是用户可选的。 具体看wiki

通过执行一下命令可以看到打印服务

 avahi-browse --all --ignore-local --resolve --terminate 

找到有用的信息如下:

+    br0 IPv4 RICOH MP C4503 [002673734B1D]                 Web Site             local
=    br0 IPv4 RICOH MP C4503 [002673734B1D]                 Web Site             local
   hostname = [RNP002673734B1D.local]
   address = [192.168.4.99]
   port = [80]
   txt = ["path=/"]

安装cupsd

启动cups-browsed.service服务后, 可以通过网页也配置打印机, 包括分享自己的打印机和添加别人的打印机。 我这里需要添加打印机服务

  1. 浏览器输入http://localhost:631/ 可以打开网页
  2. administrator -> add printer 输入root和密码
  3. 有以下选项
    Backend Error Handler
    Internet Printing Protocol (ipp)
    LPD/LPR Host or Printer
    Internet Printing Protocol (ipps)
    AppSocket/HP JetDirect
    Internet Printing Protocol (http)
    Internet Printing Protocol (https)
    Windows Printer via SAMBA 
    
  4. 我这里选择的AppSocket/HP JetDirect, 因为nmap显示是支持jetdirect的。
  5. 输入地址socket://192.168.4.99:9100
  6. 输入自定义的打印机名称和路径/

至此,浏览器打印页面就可以看到添加的打印机了

分享打印机

没有分享的时候,只有本机可以使用,当想要给手机使用时,可以勾选分享。此时CUPS会使用mdns广播打印机服务到主机所处的网段,所以手机也必须在这个网段里。 cups_share.png

更新

开始时我走的ipp协议,打印机地址为'ipp://192.168.4.99:631/ipp', 后来连不上打印机

通过查端口发现有维护人员修改了打印机端口信息, 631和80端口都被封了,可能是出于安全考虑

PORT      STATE    SERVICE
21/tcp    open     ftp
23/tcp    open     telnet
80/tcp    filtered http
139/tcp   open     netbios-ssn
514/tcp   open     shell
515/tcp   open     printer
631/tcp   filtered ipp
7443/tcp  open     oracleas-https
8080/tcp  open     http-proxy
9100/tcp  open     jetdirect
65000/tcp open     unknown
631没有开放,参考windows的同事配置,发现他们用的9100端口(9100是走socket协议),所以需要重新添加打印机
新的网络地址即为'socket://127.0.0.1:9100'

更新2

之前是自己搓的桌面环境,需要手动在CUPS的web界面添加打印机。而自从使用了完整桌面GNOME发现非常简单。GNOME自带有打印机配置工具,只需要填入打印机的IP(也许由于不在一个网段),就会自动检测支持的协议并配置好。

Pasted image 20240410175417.png

oauth2 认证说明

oauth2 是依赖第三方的认证方式, 例如玩游戏的时候弹出QQ登录,微信登录。 这种游戏运营商并不需要用户注册,而是直接从QQ或者微信那里获取用户的OPENID, 然后游戏游戏运营商存储并通过OPENID来识别用户

而且, 有资质的游戏运营商还可以通过玩家的openid来获取用户的信息, 例如用户的手机号,网名,年龄等信息。 但有资质这个是有QQ和微信来决定的,游戏运营商需要先去腾讯那里注册认证。腾讯愿意给游戏运营商分享多少信息是腾讯说了算。 所以有用户的数据在手, 腾讯稳稳当国内老大

以QQ登录为例,当玩家登录游戏时,游戏运营商先让用户访问QQ的auth2服务器,并带上游戏运营商的id。待QQ认证后会回调运营商注册的回调接口 一般为oauth/callback,并且带上用户的openid。 这样游戏运营商就知道是谁登录了。

如果游戏运营商需要更多用户资料时,例如注册, 运营商可以通过QQ的查询接口,密钥以及用户的openid 去查询。 这样就拉取到了用户的信息。 然后如果资料不全, 再让玩家补充,例如输入身份证号。这应该国家不准腾讯向别人分享的,必须要用户自己输入。。

使用github的oauth认证

  1. 先去github -> developer 创建oauth应用, 输入自己的回调地址。 当用户被github认证后,会调用这个地址

  2. 在服务端配置,利用一个开源 oauth2_proxy工具, 项目地址:

https://github.com/oauth2-proxy/oauth2-proxy
  1. 配置nginx

配置`oauth2_proxy

之前遇到问题,一直授权不成功, 原因是cookie_secure 配成了true, 即使是https下。原因未知

auth_logging = true
auth_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] [{{.Status}}] {{.Message}}"
## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
pass_basic_auth = true
# pass_user_headers = true
## pass the request Host Header to upstream
## when disabled the upstream Host is used as the Host Header
pass_host_header = true
## Email Domains to allow authentication for (this authorizes any email on this domain)
## for more granular authorization use `authenticated_emails_file`
## To authorize any email addresses use "*"
# email_domains = [
#     "yourcompany.com"
# ]
email_domains="*"
## The OAuth Client ID, Secret
provider="github"
client_id = "cef54714c84e3b0c2248"
client_secret = "a96d3d94771273b5295202d03c0c2d3ca7f625dc"
## Pass OAuth Access token to upstream via "X-Forwarded-Access-Token"
pass_access_token = false
## Authenticated Email Addresses File (one email per line)
# authenticated_emails_file = ""
## Htpasswd File (optional)
## Additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
## enabling exposes a username/login signin form
# htpasswd_file = ""
## Templates
## optional directory with custom sign_in.html and error.html
# custom_templates_dir = ""
## skip SSL checking for HTTPS requests
# ssl_insecure_skip_verify = false
## Cookie Settings
## Name     - the cookie name
## Secret   - the seed string for secure cookies; should be 16, 24, or 32 bytes
##            for use with an AES cipher when cookie_refresh or pass_access_token
##            is set
## Domain   - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
## Expire   - (duration) expire timeframe for cookie
## Refresh  - (duration) refresh the cookie when duration has elapsed after cookie was initially set.
##            Should be less than cookie_expire; set to 0 to disable.
##            On refresh, OAuth token is re-validated.
##            (ie: 1h means tokens are refreshed on request 1hr+ after it was set)
## Secure   - secure cookies are only sent by the browser of a HTTPS connection (recommended)
## HttpOnly - httponly cookies are not readable by javascript (recommended)
# cookie_name = "_oauth2_proxy"
cookie_secret = "beautyfly"
cookie_domains = "beautyflying.cn"
cookie_expire = "168h"
# cookie_refresh = ""
cookie_secure = false
# cookie_httponly = true

可以将oauth2 配置成服务

[Unit]
Description = OAuth2 proxy for www blog

[Service]
Type=simple
ExecStart=/usr/bin/oauth2_proxy -config /etc/oauth2-proxy.cfg
[Install]
WantedBy=multi-user.target

nginx 配置

    location /oauth2/ {
            proxy_pass       http://127.0.0.1:4180;
            proxy_set_header Host                    $host;
            proxy_set_header X-Real-IP               $remote_addr;
            proxy_set_header X-Scheme                $scheme;
            proxy_set_header X-Auth-Request-Redirect $request_uri;
        # or, if you are handling multiple domains:
        # proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
    }

    location = /oauth2/auth {
    proxy_pass       http://127.0.0.1:4180;
    proxy_set_header Host             $host;
    proxy_set_header X-Real-IP        $remote_addr;
    proxy_set_header X-Scheme         $scheme;
    # nginx auth_request includes headers but not body
    proxy_set_header Content-Length   "";
    proxy_pass_request_body           off;
    }

    location / {

    auth_request /oauth2/auth;
    error_page 401 = /oauth2/sign_in;

    # pass information via X-User and X-Email headers to backend,
    # requires running with --set-xauthrequest flag
    auth_request_set $user   $upstream_http_x_auth_request_user;
    auth_request_set $email  $upstream_http_x_auth_request_email;
    proxy_set_header X-User  $user;
    proxy_set_header X-Email $email;

    # if you enabled --pass-access-token, this will pass the token to the backend
    auth_request_set $token  $upstream_http_x_auth_request_access_token;
    proxy_set_header X-Access-Token $token;

    # if you enabled --cookie-refresh, this is needed for it to work with auth_request
    auth_request_set $auth_cookie $upstream_http_set_cookie;
    add_header Set-Cookie $auth_cookie;

    # When using the --set-authorization-header flag, some provider's cookies can exceed the 4kb
    # limit and so the OAuth2 Proxy splits these into multiple parts.
    # Nginx normally only copies the first `Set-Cookie` header from the auth_request to the response,
    # so if your cookies are larger than 4kb, you will need to extract additional cookies manually.
    auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;

    # Extract the Cookie attributes from the first Set-Cookie header and append them
    # to the second part ($upstream_cookie_* variables only contain the raw cookie content)
    if ($auth_cookie ~* "(; .*)") {
        set $auth_cookie_name_0 $auth_cookie;
        set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
    }

    # Send both Set-Cookie headers now if there was a second part
    if ($auth_cookie_name_upstream_1) {
        add_header Set-Cookie $auth_cookie_name_0;
        add_header Set-Cookie $auth_cookie_name_1;
    }

        root   /usr/share/nginx/html/blog;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

使用baidu 的oauth 认证