Alpine Linux Repository本地镜像制作

0. 前言

[不说废话, 只有干货]

1) 个人简介: 80年的技术人员, 毕业于美院, 从事于IT, 画了20多年画, 写了10多年代码, 专注于技术和艺术

2) 这是一个系列教程, 意在提高所有docker的爱好者工作效率, 大家可以相互交流

3) 文章肯定不会那么生动, 技术没有必要煽情

4) 这是我第一次发布程序相关的技术文章, 10年前发表过很多关于3dsmax和maya的技术文章


1. 背景

alpine linux在docker的兴起后, 应用越来越广泛, 但是由于国内网络原因, 导致在apk add的时候下载apk包会很慢, 国内也没有第三方的镜像可用, 因此构建一个自己本地的alpine镜像是很有必要的.

1) 目前alpine linux最新版本为3.3.x, 基于此版本, 我们只做3.3的本地镜像

2) 目前因为没有多少人用x86 32位的镜像, 基于此, 我们只做x86_64的镜像


2. 注意

文中多次提及线程, 其实在node.js中是没有这个概念的, 有兴趣的可以libuv拉来看下, 以后我会开源一个基于libuv来做的relay服务器源码(C++)

3. 准备

1) 找到官方镜像列表

http://rsync.alpinelinux.org/alpine/MIRRORS.txt

2) 选择其中一个, 在浏览器打开镜像(在这里我们选择官方默认镜像地址, 也就是第一个)

http://nl.alpinelinux.org/alpine/v3.3/

打开后, 我们可以看到alpine镜像分为communitymain两个类别, 每个类别下分两类x86和x86_64, 按照之前约定, 我们只对x86_64进行镜像, 如需要镜像x86的用户, 请自己根据需求调整代码

3) 进入main部分, 我们可以看到这里包括2类文件:

http://nl.alpinelinux.org/alpine/v3.3/main/x86_64/

a) APKINDEX.tar.gz (apk文件的索引文件)

b) *.apk  (apk包文件, 这些就是我们在执行apk --update add的时候下载的包文件)


4. 需求分析

要完成该工作, 需要分析几个事情:

1) 用什么语言来实现

可以用作后端的多个流行语言, 包括: c, c++, asp.net, node.js, go, python, lua等, 该项目最后选定用node.js来实现, 没有原因, 因为书写最快, 没有人愿意在准备工作上花费太多时间

2) 需要哪些模块

a) 数据库 (保存文件索引, 和下载情况)

node.js可以选用的本地文件级数据库很多, 这里选择一个类mongodb的数据库nedb (https://github.com/louischatriot/nedb)

mongodb使用比较简单, 有javascript基础的, 基本不需要学习, 难点在集群的配置和维护, 以后有时间我会分享此部分

b) http (下载索引列表, 下载文件)

http不需要选择, node.js自己带了这个库, 直接用就行, 为了快速控制http参数, 引用了request模块 (没有太大用, 只是为了开发简单)

c) mkdirp (用于创建下载文件夹)

你也可以split后递归来创建 (这里只是为了开发简单)

d) node-static(用于静态文件的http监听)

当然你还是可以自己用http模块实现 (这里只是为了开发简单)

5. 逻辑分析

1) 下载索引页面

分析出所有的文件下载地址和Last Modified的时间 (用于比较是否需要更新本地文件)

在这里, 下载地址我们只需要文件名部分, 因为我们在下载的时候, 可以用alpine的几个mirror来多线程下载, 这样效率更高

然后通过正则得出我们需要的部分, 都很简单

var regRow = /(<tr>)(.*href.*)(.*?)(<\/tr>)/ig, // 获取行
regLink = /(.*href=")(.*?)(">.*)/ig, // 获取链接
regDate = /(.*<td class="m">)(.*?)(<\/td>.*)/ig; // 获取创建时间


2) 根据索引文件列表使用http下载文件

var download = function(url, dest, thread, cb) {
    dest = unescape(dest); // 这里比较关键, 具体的自己去google一下这个方法就知道为什么要用了
    var file = fs.createWriteStream(dest);
    file.on('finish', function() {
        file.close(cb);
    });
    var request = http.get(url, function(response) {
        response.pipe(file);
    }).on('error', function(err) {
        // catch error
    });
};

如果是简单的单线程, 就这么简单了, 就写完了, 哈, 但是为了效率, 还是做成多线程的好些, 因此又多了一下几个步骤

3) 保存索引到数据库

db.update({ _id: link, date: { $ne: date } }, 
    { $set: { date: date, status: 1 } }, 
    { upsert: true, returnUpdatedDocs: true },
    function(err, num, doc) {
        
    });

这里为什么要用update而不用insert呢? 很简单, 我们要根据日期的变化来重新下载该文件, 所以这里需要使用到upsert, 官方描述得很清晰, 我这里不再多说, 一个目的: 有就update, 没有就insert

upsert (defaults to false) if you want to insert a new document corresponding to the update rules if your querydoesn't match anything. If your update is a simple object with no modifiers, it is the inserted document. In the other case, the query is stripped from all operator recursively, and the update is applied to it.

单条记录的状态我们设置为3态: 1. 未下载, 2. 已下载, 3. 正在下载

为什么会有正在下载? 很简单, 多线程, 必须得互斥

4) 从索引拉取记录, 进行下载

做过多线程的都知道, 要么加锁, 要么就事务(其实也是锁), 在这里我们使用nedb的update来简单实现:

db.update({ status: 1 }, 
	{ $set: { status: 3 } }, 
	{ returnUpdatedDocs: true }, 
	function(err, num, doc) {
		
	});


returnUpdatedDocs 可以返回收到影响的记录, 官方也哟很清晰的描述, 不在多说, 一个目的, 抢到一个文件下载地址, 并且不让别人抢到: 

returnUpdatedDocs (defaults to false, not MongoDB-compatible) if set to true and update is not an upsert, will return the array of documents matched by the find query and updated. Updated documents will be returned even if the update did not actually modify them.

5) 下载完成后, 更新文件下载状态

db.update({ _id: doc._id }, 
    { $set: { status: 2 } }, 
    function(err) {

    });

6) 如果程序crash了怎么办?

我们可以在docker run的时候加上--restart=always来keep alive, 但是restart后, 还是得重新启动下载, 因此我们需要一个循环检查过程

db.update({ status: { $in: [3, 1] } }, 
    { $set: { status: 1 } }, 
    { multi: true },
    function(err, num) {
        // 通过影响的行数(num), 来判断是不是需要马上下载
    });

把之前处于正在下载和未下载的都更新到未下载状态, 然后再调用线程下载文件

为什么要把未下载的({status: 1})再更新一次呢? 很简单, 我们需要通过影响的行数来判断是否需要立即启动下载线程, 如果行数为0, 那么我们就只需要等待拉取列表, 如果获取到了新文件才启动下载线程

7) 还需要启动一个tiny http server, 这个太简单了(用node-static直接搞定), 不多说:

var static = require('node-static'),
    port = 8098,
    http = require('http');

// config
var file = new static.Server('./files', {
    cache: 3600,
    gzip: true
});

// serve
http.createServer(function(request, response) {
    request.addListener('end', function() {
        file.serve(request, response);
    }).resume();
}).listen(port);

6. 源码

http://git.oschina.net/null_855_3980/alpine-repo

7. 结语

此项目, 我们牵涉到了几个点, 1) node.js, 2) mongodb, 3) http server, 当然都是基础下的基础

还有就是提供一个懒人方案, 直接用wget搞定一起, :-)

挂到后台, 几个小时也就下来了, 哈哈

wget -r -np -nH http://nl.alpinelinux.org/alpine/v3.3/main/x86_64/

你可能感兴趣的:(node.js,docker,Alpine)