Linux网络编程IO模型

IO

IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间<–>内核空间、内核空间<–>设备空间(磁盘、网络等)。

IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。

LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。

对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。

所以,对于一个网络输入操作通常包括两个不同阶段:

等待网络数据到达网卡→读取到内核缓冲区,数据准备好;
从内核缓冲区复制数据到进程空间。

阻塞IO模型

进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程,操作成功则进程获取到数据。

非阻塞IO模型

进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞,进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。

IO复用模型

进程的多个IO可以注册到一个复用器(select)上,然后调用该select, select会监听所有注册进来的IO。
如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞,而当任一IO在内核缓冲区中有可数据时,select调用就会返回,而后进程再次发起读取IO,读取内核中准备好的数据。

信号驱动IO模型

当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞,当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。

异步IO模型

当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结,内核把整个IO处理完后,会通知进程结果,如果IO操作成功则进程直接获取到数据。

IO模型比较

同步IO:导致请求进程阻塞,直到I/O操作完成。
异步IO:不导致请求进程阻塞。

阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型都为为同步IO,只有异步IO模型是异步IO。

阅读全文

使用树莓派搭建网站

整体架构

公网入口

树莓派服务暴露到公网,使用的方案是用Frp做内网穿透,在阿里云的公网ECS上搭建了Frp Server,树莓派上跑了Frp Client,实现将ECS的7050端口转发到树莓派的80端口上,在树莓派的80端口启动了Nginx,这样访问ECS的7050端口,就访问到了树莓派上的Nginx上。

ECS上同时托管了其他网站,统一用了Traefik做入口代理服务器监听在了ECS的80端口,便在Traefik上添加树莓派域名并配置反向代理到本地的7050端口,实现通过域名访问到树莓派的Nginx服务。

树莓派上的Nginx服务可以通过公网访问,之后在树莓派内启动的其他服务,都可以通过这个Nginx做转发暴露出去。

Web服务

server {
        listen 80 default_server;

        server_name raspberrypi.v95.xyz;

        client_max_body_size    10m;

        root /home/pi/www/public;

        index index.html index.htm;

        location ^~ /.git {
                return 403;
        }

        location ~ \.php {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/var/run/php/php-fpm.sock;
        }

        location ~ /api {
              proxy_pass http://127.0.0.1:5050;
        }
}

Nginx上配置public目录作为网站根目录,静态文件放到这个目录就可以,安装php-fpm配置fastcgi_pass实现同时支持执行php脚本。其他在树莓派上用python开发的api服务,使用gunicron跑在了本地的5050端口,加上配置api路径proxy_pass到本地的5050端口。

CDN与HTTPS

实测外部打开网站的所有请求都到树莓派上速度还可以,请求百毫秒左右差不多都可以响应,可见现在公网质量可以。但考虑树莓派的IO性能拉跨和节省ECS弹性IP流量,优化网站的访问体验,便在阿里云给树莓派域名配置了CDN同时申请了免费的SSL证书配置在CDN上,实现了网站访问加速和支持HTTPS.

阅读全文

树莓派的启动过程

计算机启动过程

以嵌入式芯片(MCU)为例,芯片的指令芯片厂商已经通过电路硬件实现在了芯片内部,自己实现的软件,要通过芯片对应的编译器(汇编、C语言)编译成该芯片可以识别的指令,最后写到芯片的Rom里。芯片上电后,CPU会自动去Rom里取指令加载到Ram然后不停的执行。

嵌入式芯片在一个芯片里把我们日常用的PC机的组件都做进去了,CPU,内存(Ram),硬盘(Rom),并且在CPU内固化好逻辑告诉CPU上电后到指定位置取用户存放的代码,放到内存指定开始位置,然后开始执行。

我们日常用的PC机,CPU、内存、硬盘都是分开的,硬盘系统不仅可以重复安装还可以格式化为不同文件系统装不同操作系统,哪里实现的加载逻辑呢? PC上电后CPU从哪里开始执行程序?

PC机的CPU也有这个固化逻辑从哪里取代码、放到内存哪个位置开始执行,只是第一个加载的程序不是用户硬盘中放的程序,而是主板的ROM中存放的系统程序(BIOS程序)。

CPU先取到BIOS的代码执行,BIOS程序完成系统检测等工作,然后按照用户的设置引导CPU接下来上那块硬盘,那个区,那个文件取代码,放到那个内存地址,最后从那个内存地址开始执行。

CPU执行BIOS程序沿着BIOS的引导在硬盘上找到程序放入0x7c00处开始的内存位置,然后跳转到0x7c00开始执行。注意这个程序通常还不是操作系统程序,而是我们常说的“引导”程序,比如GRUB。

CPU继续执行引导程序,沿着引导程序的引导加载操作系统代码到内存,并从某时刻跳转到操作系统main函数,至此BIOS和引导程序的使命完成,计算机内的天下全部变成操作系统的了。

相比之下:

MCU:Rom中的程序 = PC机:BIOS程序+引导程序+操作系统程序。

为什么PC机的启动这么复杂,其实还是计算机解耦分层的思想。PC上BIOS程序最简单但也比MCU的程序复杂多,要实现指引CPU上那块硬盘,硬盘分区是MBR还GPT的哪里找接下来的程序,让CPU能读硬盘BIOS还要实现支持硬盘分区的文件系统等等功能。接下来引导程序,作为中间层可以解耦BIOS和操作系统的绑定关系,相信大家学习Linux时候应该装过Windows和Linux双系统吧,开机时候是不是要在GRUB界面选择要启动的操作系统。

树莓派启动过程

了解了PC的启动过程,参考树莓派的文档,便很容易理解树莓派的启动过程。有意思的是,树莓派的启动过程不是CPU完成的,而是用的GPU,当然树莓派的CPU和GPU是集成在一起的。

首先,系统芯片加载固化程序(Boot Rom), 这个程序在支持的启动设备(SD卡、USB等)中寻找启动程序文件bootcode.bin,把bootcode.bin加载到Cache中并运行它,

然后,bootcode.bin程序会检索程序start.elf,然后运行它。

最后,start.elf程序将kernel.img加载到内存中,内核开始运行,系统启动完成。

其中需要注意,存放bootcode.bin、start.elf的分区需要是FAT32(所以明白了平时做的树莓派系统卡一个是FAT32分区,一个是Ext4),树莓派4因为内置了EEPROM芯片存放bootcode.bin程序,所以树莓派4会忽略bootcode.bin,直接加载EEPROM芯片中的代码。

另外start.elf程序支持用户配置config.txt,实现树莓派系统配置,同时支持配置加载kernel.img时的参数cmdline.txt。

当系统完全启动后,启动相关的文件和程序,可以在/boot目录下找到,用户可以修改config.txt和cmdline.txt然后重启树莓派配置一些初始化参数。

树莓派更新固件和内核

经过一番研究,顺便还发现,树莓派启动相关的代码,是不开源的,系统定制的kernel代码是开源的,bootcode.bin,start.elf官方放到Github上的都是二进制!要重新认识树莓派了,不完全称得上是开源硬件。

网上讨论,树莓派的芯片是博通专供且这部分固件代码也不开源,树莓派的自由与方便差不多是Kernel之上的自由和方便。基金会和博通这么干,也是市面上没有树莓派仿品板子的原因。

既然代码不开源,官方提供了更新这部分软件的通道,使用rpi-update命令可以更新官方编译好的kernel和/boot目录下的相关固件文件,树莓派4更新EEPROM可以使用rpi-eeprom-update命令。

阅读全文

kubernetes元数据存储之kine(二)

kine中的存储Driver最终需要对接Log接口,以SQLite为例,分析一下,kine如何基于关系数据库实现的mvccdb。

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/logstructured.go

type Log interface {
    Start(ctx context.Context) error
    CurrentRevision(ctx context.Context) (int64, error)
    List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeletes bool) (int64, []*server.Event, error)
    After(ctx context.Context, prefix string, revision, limit int64) (int64, []*server.Event, error)
    Watch(ctx context.Context, prefix string) <-chan []*server.Event
    Count(ctx context.Context, prefix string) (int64, int64, error)
    Append(ctx context.Context, event *server.Event) (int64, error)
    DbSize(ctx context.Context) (int64, error)
}

表设计和字段映射

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/drivers/sqlite/sqlite.go
CREATE TABLE IF NOT EXISTS kine
(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name INTEGER,
    created INTEGER,
    deleted INTEGER,
    create_revision INTEGER,
    prev_revision INTEGER,
    lease INTEGER,
    value BLOB,
    old_value BLOB
)

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/types.go

type KeyValue struct {
    Key            string
    CreateRevision int64
    ModRevision    int64
    Value          []byte
    Lease          int64
}

type Event struct {
    Delete bool
    Create bool
    KV     *KeyValue
    PrevKV *KeyValue
}
Event 表字段
KV.ModRevision id
KV.Key name
KV.Create created
KV.Delete deleted
KV.CreateRevision create_revision
PrevKV.ModRevision prev_revision
KV.Lease lease
KV.Value value
PrevKV.Value old_value
  • 主键自增id为该记录KeyValue的Revision。
  • KeyValue创建记录created=1、deleted=0、create_revision=0、prev_revision=0。
  • KeyValue更新记录create_revison为created=1记录id,prev_revision为前记录id。
  • KeyValue删除记录deleted=1,create_revison=创建记录id,prev_revision=前记录id。
  • value字段和old_value字段记录,当前版本和上一版本KeyValue值内容。

MVCC实现

查询Key

获取当前版本KeyValue即:查询name为key,最新且deleted=0的记录。

SELECT *
FROM kine AS kv
JOIN (
   SELECT MAX(mkv.id) AS id FROM kine AS mkv WHERE mkv.name LIKE ? GROUP BY mkv.name
) AS maxkv ON maxkv.id = kv.id
WHERE
kv.deleted = 0

写入Key

乐观无锁、无事务并发安全写入:name和prev_revision建立联合唯一索引。

所有Log.Append执行写入Event均携带PrevKV信息,PrevKV.ModRevision作为新记录的prev_revision字段,因为kine_name_prev_revision_uindex联合唯一索引限制并发将冲突报错,仅有一条写入成功。

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/drivers/sqlite/sqlite.go

`CREATE UNIQUE INDEX IF NOT EXISTS kine_name_prev_revision_uindex ON kine (name, prev_revision)`

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/logstructured.go

func (l *LogStructured) Update(ctx context.Context, key string, value []byte, revision, lease int64) (revRet int64, kvRet *server.KeyValue, updateRet bool, errRet error) {
	...
    rev, event, err := l.get(ctx, key, "", 1, 0, false)
	...
    updateEvent := &server.Event{
        KV: &server.KeyValue{
            Key:            key,
            CreateRevision: event.KV.CreateRevision,
            Value:          value,
            Lease:          lease,
        },
        PrevKV: event.KV,
    }

    rev, err = l.log.Append(ctx, updateEvent)
   ...
}

获取全局Revision

表最大id作为全局Revision。

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/drivers/generic/generic.go

SELECT MAX(rkv.id) AS id
FROM kine AS rkv

获取全局CompactRevision

内置keycompact_rev_key记录已压缩版本。

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/drivers/generic/generic.go

SELECT MAX(crkv.prev_revision) AS prev_revision
FROM kine AS crkv
WHERE crkv.name = 'compact_rev_key'

获取Key列表

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/sqllog/sql.go

func (s *SQLLog) List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeleted bool) (int64, []*server.Event, error) {
  	...
	//获取Key列表
    if revision == 0 {
        rows, err = s.d.ListCurrent(ctx, prefix, limit, includeDeleted)
    } else {
        rows, err = s.d.List(ctx, prefix, startKey, limit, revision, includeDeleted)
    }
    if err != nil {
        return 0, nil, err
    }

	//处理版本已压缩返回
    if revision > 0 && len(result) == 0 {
        // a zero length result won't have the compact revision so get it manually
        compact, err = s.d.GetCompactRevision(ctx)
        if err != nil {
            return 0, nil, err
        }
    }

    if revision > 0 && revision < compact {
        return rev, result, server.ErrCompacted
    }
	...
}
SELECT *
FROM (
	SELECT (rev), (compactRev), columns
	FROM kine AS kv
	JOIN (
		SELECT MAX(mkv.id) AS id
		FROM kine AS mkv
		WHERE
			mkv.name LIKE [?keyPrefix]
		GROUP BY mkv.name) AS maxkv
		ON maxkv.id = kv.id
	WHERE
		kv.deleted = 0 OR
		[?includeDeleted]
) AS lkv
ORDER BY lkv.theid ASC
LIMIT [?limit]

Watch实现

Watch gRPC接口复用etcd的代码,重新实现Handle函数。

SQLLog实现Watch内部,通过读写事件和定时轮询(默认一秒)实现Watch事件推送。

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/watch.go

func (s *KVServerBridge) Watch(ws etcdserverpb.Watch_WatchServer) error {
	w := watcher{
		server:  ws,
		backend: s.limited.backend,
		watches: map[int64]func(){},
	}
	...
	for {
		...
		if msg.GetCreateRequest() != nil {
			w.Start(ws.Context(), msg.GetCreateRequest())
		} else if msg.GetCancelRequest() != nil {
			...
		}
	}
}
...

func (w *watcher) Start(ctx context.Context, r *etcdserverpb.WatchCreateRequest) {
	...
	go func() {
		...
		for events := range w.backend.Watch(ctx, key, r.StartRevision) {
			...
		}
		...
	}()
}
//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/sqllog/sql.go

func (s *SQLLog) Watch(ctx context.Context, prefix string) <-chan []*server.Event {
    //broadcastern对象内部回调startWatch
    values, err := s.broadcaster.Subscribe(ctx, s.startWatch)
	...
    return res
}

func (s *SQLLog) startWatch() (chan interface{}, error) {
    pollStart, err := s.d.GetCompactRevision(s.ctx)
   ...
    go s.compactor(compactInterval)
    go s.poll(c, pollStart)
    return c, nil
}

func (s *SQLLog) poll(result chan interface{}, pollStart int64) {
	...
    wait := time.NewTicker(time.Second)
    defer wait.Stop()
    defer close(result)

    for {
        if waitForMore {
            select {
            case <-s.ctx.Done():
                return
            case check := <-s.notify:
                if check <= last {
                    continue
                }
            case <-wait.C:
            }
        }
		...
	}
}
阅读全文

kubernetes元数据存储之kine(一)

Kine是k3s-io为实现轻量化部署kubernetes而实现的一个支持kubernetes api-server直接读写,数据持久化到SQLite、MySQL、Postgres等后端的中间件。

Kubernetes api-server使用etcd作为后端存储集群的元数据,并且其controller机制强依赖etcd的watch机制。

Kine实现对etcd的替代,完整实现了kubernetes api-server用到的etcd接口。

透过kine这个轻量的中间件,既可以深入部分etcd原理,又可以了解kubernetes核心controller机制背后的支撑。

Etcd到Kine

所谓知己知彼,要替换etcd,首先就要了解etcd的实现本身。

如图,etcd核心分三个模块:

  • gRPC KV Server 基于gRPC的接口层
  • Raft 实现强一致的Raft日志层
  • mvccdb 基于boltdb的mvcc存储层

Etcd的读写事务请求,客户端通过gRPC发送给etcd server,etcd server形成raft日志提交给raft集群,最后etcd server apply日志,持久化数据到boltdb。

Etcd实现可靠和高性能watch,一方面基于gRPC/http2网络,一方面是数据存储的mvcc机制。

Kine实现了部分etcd相同接口,gRPC Server部分复用了etcd的gRPC代码,没有实现Raft日志模块,但是抽象了Backend层对接后端不同存储Driver,存储Driver需要实现支持mvcc的读写。

Kine Server

kine复用了etcd的go.etcd.io/etcd/api/v3/etcdserverpb所有代码实现etcd gRPC Server。

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/server.go

type KVServerBridge struct {
    limited *LimitedServer
}

...

func (k *KVServerBridge) Register(server *grpc.Server) {
    ...
    etcdserverpb.RegisterWatchServer(server, k)
    etcdserverpb.RegisterKVServer(server, k)
}

KVServerBridge 实现了gRPC功能接口函数。

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/kv.go

func (k *KVServerBridge) Range(ctx context.Context, r *etcdserverpb.RangeRequest) (*etcdserverpb.RangeResponse, error) {
   ...
    resp, err := k.limited.Range(ctx, r)
    if err != nil {
        logrus.Errorf("error while range on %s %s: %v", r.Key, r.RangeEnd, err)
        return nil, err
    }
    ...
    return rangeResponse, nil
}

LimitedServer对接Backend存储接口


// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/limited.go

type LimitedServer struct {
    backend Backend
    scheme  string
}

func (l *LimitedServer) Range(ctx context.Context, r *etcdserverpb.RangeRequest) (*RangeResponse, error) {
    if len(r.RangeEnd) == 0 {
        return l.get(ctx, r)
    }
    return l.list(ctx, r)
}

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/types.go

type Backend interface {
    Start(ctx context.Context) error
    Get(ctx context.Context, key, rangeEnd string, limit, revision int64) (int64, *KeyValue, error)
    Create(ctx context.Context, key string, value []byte, lease int64) (int64, error)
    Delete(ctx context.Context, key string, revision int64) (int64, *KeyValue, bool, error)
    List(ctx context.Context, prefix, startKey string, limit, revision int64) (int64, []*KeyValue, error)
    Count(ctx context.Context, prefix string) (int64, int64, error)
    Update(ctx context.Context, key string, value []byte, revision, lease int64) (int64, *KeyValue, bool, error)
    Watch(ctx context.Context, key string, revision int64) <-chan []*Event
    DbSize(ctx context.Context) (int64, error)
}


//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/get.go

func (l *LimitedServer) get(ctx context.Context, r *etcdserverpb.RangeRequest) (*RangeResponse, error) {
    if r.Limit != 0 && len(r.RangeEnd) != 0 {
        return nil, fmt.Errorf("invalid combination of rangeEnd and limit, limit should be 0 got %d", r.Limit)
    }
    rev, kv, err := l.backend.Get(ctx, string(r.Key), string(r.RangeEnd), r.Limit, r.Revision)
   ...
    return resp, nil
}

Kine Backend

kine在pkg/logstructuredpkg/drivers中实现对接各种后端存储。

LogStructured实现Server的Backend接口,通过Log接口和底层Driver对接。


// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/logstructured.go

type Log interface {
    Start(ctx context.Context) error
    CurrentRevision(ctx context.Context) (int64, error)
    List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeletes bool) (int64, []*server.Event, error)
    After(ctx context.Context, prefix string, revision, limit int64) (int64, []*server.Event, error)
    Watch(ctx context.Context, prefix string) <-chan []*server.Event
    Count(ctx context.Context, prefix string) (int64, int64, error)
    Append(ctx context.Context, event *server.Event) (int64, error)
    DbSize(ctx context.Context) (int64, error)
}

type LogStructured struct {
    log Log
}

func (l *LogStructured) Update(ctx context.Context, key string, value []byte, revision, lease int64) (revRet int64, kvRet *server.KeyValue, updateRet bool, errRet error) {
   ...
    rev, err = l.log.Append(ctx, updateEvent)
    ...
    return rev, updateEvent.KV, true, err
}

Backend最终向Driver通过Log.Append使用Event结构存储数据。

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/types.go

type KeyValue struct {
    Key            string
    CreateRevision int64
    ModRevision    int64
    Value          []byte
    Lease          int64
}


type Event struct {
    Delete bool
    Create bool
    KV     *KeyValue
    PrevKV *KeyValue
}

kine中EventKeyValue设计了和Etcd中keyIndex和mvccpb.KeyValue类似的可以记录版本信息的数据结构。存储Driver在执行数据存储动作时候,需要对接相关字段。

阅读全文