文献总结 - [2015] Large-scale cluster management at Google with Borg

原文链接:Large-scale cluster management at Google with Borg

一、主旨

Borg是Google内部从21世纪初就开始研发的大规模集群管理系统,属于底层基础架构,服务上层业务应用的部署和运行。
Borg对K8s有深远意义,从某种意义上可以说Borg是K8s的前身,但两者并不一样,且时至今日在Google内部Borg也并没有被K8s替代,应当也没有被替代的趋势。

二、内容

三、对K8s的影响

  1. 应用级别的多构件关联
    1. Borg中只有job概念可以串联多构件
    2. K8s中可通过label来松散串联多构件
  2. 网络地址空间
    1. Borg中所有任务共用节点IP和端口,容器端口直接映射节点端口,空间狭小
    2. K8s中pod自带网络地址空间,上层还有svc可以封装和控制是否映射到节点端口
  3. 用户倾向
    1. Borg最初目的是优先服务大团队
    2. K8s社区决定玩家结构
  4. 多容器的调度单元——pod
    1. Alloc in Borg: app + logsaver + dataloader 模式,分段开发维护
    2. Pod in K8s: init containers + helper containers/sidecars
  5. 运维的可观测辅助
    1. Debugging information
    2. Events
  6. Master即内核——Borgmaster in Borg = kube-apiserver in K8s

四、同类系统

SysName Ref
Apache Mesos B. Hindman, A. Konwinski, M. Zaharia, A. Ghodsi,A. Joseph, R. Katz, S. Shenker, and I. Stoica.Mesos: aplatform for fine-grained resource sharing in the data center.InProc. USENIX Symp. on Networked Systems Design andImplementation (NSDI), 2011
YARN V. K. Vavilapalli, A. C. Murthy, C. Douglas, S. Agarwal,M. Konar, R. Evans, T. Graves, J. Lowe, H. Shah, S. Seth,B. Saha, C. Curino, O. O’Malley, S. Radia, B. Reed, andE. Baldeschwieler.Apache Hadoop YARN: Yet AnotherResource Negotiator.InProc. ACM Symp. on CloudComputing (SoCC), Santa Clara, CA, USA, 2013.
Tupperware A. Narayanan.Tupperware: containerized deployment atFacebook.http://www.slideshare.net/dotCloud/tupperware-containerized-deployment-at-facebook,June 2014.
Apache Aurora (retired) Apache Aurora.http://aurora.incubator.apache.org/, 2014.
Autopilot https://aurora.apache.org/
Quincy (on Borg) M. Isard, V. Prabhakaran, J. Currey, U. Wieder, K. Talwar,and A. Goldberg.Quincy: fair scheduling for distributedcomputing clusters.InProc. ACM Symp. on OperatingSystems Principles (SOSP), 2009.
Cosmos P. Helland.Cosmos: big data and big challenges.http://research.microsoft.com/en-us/events/fs2011/helland\_cosmos\_big\_data\_and\_big\_challenges.pdf, 2011.
Apollo E. Boutin, J. Ekanayake, W. Lin, B. Shi, J. Zhou, Z. Qian,M. Wu, and L. Zhou.Apollo: scalable and coordinatedscheduling for cloud-scale computing.InProc. USENIXSymp. on Operating Systems Design and Implementation(OSDI), Oct. 2014.
Fuxi Z. Zhang, C. Li, Y. Tao, R. Yang, H. Tang, and J. Xu.Fuxi: afault-tolerant resource management and job schedulingsystem at internet scale.InProc. Int’l Conf. on Very LargeData Bases (VLDB), pages 1393–1404. VLDB EndowmentInc., Sept. 2014.
Omega M. Schwarzkopf, A. Konwinski, M. Abd-El-Malek, andJ. Wilkes.Omega: flexible, scalable schedulers for largecompute clusters.InProc. European Conf. on ComputerSystems (EuroSys), Prague, Czech Republic, 2013.
Kubernetes https://kubernetes.io/

五、文献引申

  1. 大规模系统的关键因素:J. Hamilton.On designing and deploying internet-scaleservices.InProc. Large Installation System AdministrationConf. (LISA), pages 231–242, Dallas, TX, USA, Nov. 2007.
  2. 系统性能评测的建模:D. G. Feitelson.Workload Modeling for Computer SystemsPerformance Evaluation.Cambridge University Press, 2014.
  3. 资源利用率相关指标和实验:A. Verma, M. Korupolu, and J. Wilkes.Evaluating jobpacking in warehouse-scale computing.InIEEE Cluster,pages 48–56, Madrid, Spain, Sept. 2014.
  4. 资源调度worst-fit:Y. Amir, B. Awerbuch, A. Barak, R. S. Borgstrom, andA. Keren.An opportunity cost approach for job assignmentin a scalable computing cluster.IEEE Trans. Parallel Distrib.Syst., 11(7):760–768, July 2000.
  5. 真实工作负载记录(数据集):J. Wilkes.More Google cluster data.http://googleresearch.blogspot.com/2011/11/more-google-cluster-data.html, Nov. 2011.
  6. 上述数据集的使用:
    1. 数据集分析:C. Reiss, A. Tumanov, G. Ganger, R. Katz, and M. Kozuch.Heterogeneity and dynamicity of clouds at scale: Googletrace analysis.InProc. ACM Symp. on Cloud Computing(SoCC), San Jose, CA, USA, Oct. 2012.
    2. 使用:O. A. Abdul-Rahman and K. Aida.Towards understandingthe usage behavior of Google cloud users: the mice andelephants phenomenon.InProc. IEEE Int’l Conf. on CloudComputing Technology and Science (CloudCom), pages272–277, Singapore, Dec. 2014.
    3. 使用:S. Di, D. Kondo, and W. Cirne.Characterization andcomparison of cloud versus Grid workloads.InInternationalConference on Cluster Computing (IEEE CLUSTER), pages230–238, Beijing, China, Sept. 2012.
    4. 使用:S. Di, D. Kondo, and C. Franck.Characterizing cloudapplications on a Google data center.InProc. Int’l Conf. onParallel Processing (ICPP), Lyon, France, Oct. 2013.
    5. 使用:Z. Liu and S. Cho.Characterizing machines and workloadson a Google cluster.InProc. Int’l Workshop on Schedulingand Resource Management for Parallel and DistributedSystems (SRMPDS), Pittsburgh, PA, USA, Sept. 2012.
  7. Borg后续
    1. 2016_Burns_Borg, Omega, and Kubernetes: Lessons learned from three container-management systems over a decade
    2. 2020_Tirmazi_Borg: the next generation

Failed to initialize NVML: Driver/library version mismatch

显卡驱动与NVML库版本不一致问题。

可到/usr/lib/x86_64-linux-gnu/路径下查找libnvidia-ml*相关软链和文件:

1
2
3
4
5
root@10-101-72-17:/usr/lib/x86_64-linux-gnu# ll | grep libnvidia-ml
lrwxrwxrwx 1 root root 17 Jan 29 01:08 libnvidia-ml.so -> libnvidia-ml.so.1
lrwxrwxrwx 1 root root 26 Jan 29 01:08 libnvidia-ml.so.1 -> libnvidia-ml.so.470.103.01
-rw-r--r-- 1 root root 1828056 Jan 6 20:11 libnvidia-ml.so.470.103.01
-rwxr-xr-x 1 root root 1823960 Nov 15 15:58 libnvidia-ml.so.470.42.01*

一般libnvidia-ml.so为软链指向libnvidia-ml.so.1又为软链指向一个libnvidia-ml.so.xxx.yyy.zzz的文件,xxx.yyy.zzz为NVML版本号。

如果该目录下存在多个不同版本号的NVML动态链接库文件(如上述代码段中所示),则有可能手动安装过显卡驱动并随驱动安装了一个版本的NVML,而系统又因各种原因新安装了另一个版本,导致驱动版本与NVML版本不匹配。

这里一个可能的原因是Ubuntu的系统自动更新,可通过查阅/var/log/apt/history.log日志,看是否自动更新过libnvidia或libcuda等可能与显卡相关的组件。

简单粗暴的解决方案:重启节点并重新安装驱动,手动安装或apt均可,保证将驱动和NVML刷成一致的版本即可。

另一个不想重启的解决思路(未验证):如果是手动安装的驱动,由系统更新造成了版本问题,不重启时直接重装原来装过的驱动时会失败,或许可以下载与系统更新出的NVML版本完全一致的驱动文件再手动安装。

Linux传输工具rsync

简介

远程传输工具rsync的命令语法与scp相似,原理与scp不同,最主要的特性是支持增量传输和断点续传(-P选项),先抄一段帮助文档的使用说明:

1
2
3
4
5
6
7
8
9
Usage: rsync [OPTION]... SRC [SRC]... DEST
or rsync [OPTION]... SRC [SRC]... [USER@]HOST:DEST
or rsync [OPTION]... SRC [SRC]... [USER@]HOST::DEST
or rsync [OPTION]... SRC [SRC]... rsync://[USER@]HOST[:PORT]/DEST
or rsync [OPTION]... [USER@]HOST:SRC [DEST]
or rsync [OPTION]... [USER@]HOST::SRC [DEST]
or rsync [OPTION]... rsync://[USER@]HOST[:PORT]/SRC [DEST]
The ':' usages connect via remote shell, while '::' & 'rsync://' usages connect
to an rsync daemon, and require SRC or DEST to start with a module name.

在做目录级的传输时,需要特别注意的是,源目录路径末尾有没有斜杠是会有不同效果的:

  • 带斜杠,如/data/,则同步/data目录下的所有文件到目标路径下
  • 不带斜杠,如/data,则同步/data目录到目标路径下,作为子目录

一些有用的options

  • -a 全家桶选项,组合了多个其他选项(-rlptgoD),基本无脑使用,并且对于目录来说-a是包含-r
  • -z 对于未压缩的文件做压缩传输(gzip)
  • -P 断点续传
  • -e 通过ssh连接时,如果需要走key,可以写成-e "ssh -i /path/to/sshkey"
  • --bwlimit 限制网络传输速度,值的单位为KB/s

例子

Push to remote

1
rsync -azP /data/ user@1.2.3.4:/data

将本地/data目录下的所有文件推送到远程1.2.3.4机器的/data目录下。

Pull from remote

1
rsync -e "ssh -i ~/.ssh/mykey" --bwlimit=3000 -azP user@1.2.3.4:/data/mydata.tar /data/

~/.ssh/mykey作为ssh key,限制速度约为3MB/s,将远程1.2.3.4机器的/data/mydata.tar文件拉取到本地/data目录下。

Kubernetes Finalizer机制

一、Finalizer简介

Finalizer是K8s资源删除流程中的一种控制机制

在现实中,K8s中的资源往往不是完全独立和无状态的,2个资源对象之间可能会有依赖关系,随意删除一个对象可能会对依赖它的其他对象产生影响。因此,这些复杂的资源对象的删除流程也需要引入复杂的处理逻辑。

Finalizer就是服务于这类需求的一种机制,它可以在资源的删除过程中增加一个步骤,为这类复杂的删除逻辑的实现提供了可能。

举一个不太严格的例子,这里我们可以类比RDBMS里的外键:book表的作者字段外键关联到writer表。当我们不定义外键的ON DELETE逻辑时,一个writer条目是无法先于他/她的著作books被删除的。我们有至少两种处理writer deletion的方式:(1)定义外键ON DELETE,可以是CASCADESET NULL等等;(2)在外部系统中实现先删除books再删除writer的逻辑。两种处理方式的根本思想都是,在切实地删除writer之前,先“清理干净”它的依赖关系。Finalizer就可以想象成这个“清理”操作的插槽。

再举一个不太严格的例子,我们还可以类比很多面向对象的编程框架中的对象生命周期钩子(hook),比如Android框架中Activity的onDestroy和Vue框架中的beforeDestroy,在一个对象即将被删除之前,调用一个通过钩子函数/方法/扩展插槽定义的一套处理逻辑。Finalizer也可以想象成这种钩子插槽。

二、Finalizer原理

实际上,Finalizer的原理非常简单优雅,它在K8s对象上的存在形式其实是一系列标签,类似annotations。它本身并不定义流程细节或实现具体逻辑。K8s在接收到一个资源对象删除请求时,会先在对象上打上一些标记,包括deletionTimestamp,表示该对象已进入删除流程,当检测到对象上有finalizers标签(通常在资源对象的metadata字段中)时,删除流程会被挂起,直到所有finalizers标签被移除时,才继续进行删除流程(后面还会经历其他类似的阶段,例如ownerRefenreces,最终才会实际删除对象),如下图所示:

(Finalization状态转移图,引自:https://kubernetes.io/blog/2021/05/14/using-finalizers-to-control-deletion/)

在删除流程因finalizers标签被挂起时,对象控制器(controller-manager)通过对finalizer标签的捕获,可以得知这里可能有在删除对象之前需要处理的事情,再由控制器或其他组件/控制器实际去处理这些事情。当控制器认为所有该处理的事情都处理得当的时候,控制器来移除finalizers标签,从而将删除流程进行下去,直到最终实际删除掉对象。处理finalizers的思路是很开阔的:可以是主动地由本对象的controller来直接处理相关的清理工作,也可以是被动地等待外部组件或其他对象的控制器去完成它们的工作。

三、K8s原生Finalizer示例

K8s的一些原生资源对象会自动被加上一些finalizers。由于这些对象的控制器也是原生的,在不做扩展的情况下,这些原生的finalizers都是被定义好的,不应随意添加不被原生控制器识别的finalizers,避免无法删除对象的问题。

  1. PVC和PV分别原生自带kubernetes.io/pvc-protectionkubernetes.io/pv-protectionfinalizers标签的,顾名思义,其目的在于保护持久化存储不被误删,避免挂载了存储的工作负载产生问题。

  2. Namespace也是自带一个kubernetes的finalizers标签的,只不过,不同于其他资源对象的metadata.finalizers标签,ns是spec.finalizers,其作用是相同的。

四、某系统中的Finalizer实践

类似Pod-PVC-PV的挂载依赖关系,实际工作的某系统当中的后端组件(以CRD进行了抽象,称Backend)与自研存储(亦以CRD进行了抽象,称Cave)之间也有这层挂载依赖。在日常的开发调试以及部署时的蓝绿、红黑、灰度等过程中,经常要对后端组件进行卸载重装。如果在卸载时,先删除了Cave的话,那么挂载了它的后端容器便无法优雅删除。这是因为Cave本质是基于NFS的文件存储,而NFS的设计是在客户端unmount阶段中依然需要NFS服务端进行响应,若Cave已删除,则相当于NFS服务端失去响应,那么客户端的unmount操作会一直被挂起。

不止于此,由于Kubelet是处理工作负载的总控制程序,unmount这个操作也是由它去实际执行的,unmount的挂起也造成Kubelet处理unmount的子进程被挂起,从而导致Kubelet本身也无法再优雅停止。当然,这并不会造成集群系统的不可用,但当我们需要重启Kubelet时,就会造成Kubelet与集群失联。这也是之前某次生产集群开启Feature Gates时真实遇到的问题(因需要重启Kubelet)。比较straightforward的解决方法就是重启大法……但可想而知,这会对集群的可用性产生影响。

因此,我们应当确保在删除一个Cave组件之前,挂载了它的后端工作负载均已被删除。这个思想与pv-protection如出一辙。

我们之前已有Operator实现了Cave的控制器,Cave的Finalizer机制便是在Cave控制器的调谐方法中进行扩展的。

cave_controller.goview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
caveProtectionFinalizer := "abc/cave-protection"

// IN RECONCILATION

// Some reconcilation logic in before...

if cave.ObjectMeta.DeletionTimestamp.IsZero() {
// Cave is not being deleted:
// append finalizer to protect cave from being deleted before backend & hpo
// otherwise backend & hpo might fail to unmount cave
// which make kubelet hang
if !containsFinalizer(cave.GetFinalizers(), caveProtectionFinalizer) {
controllerutil.AddFinalizer(&cave, caveProtectionFinalizer)
if err := r.Update(ctx, &cave); err != nil {
reqLogger.Error(err, "unable to add finalizer on cave resource", "cave", cave.Name)
}
return ctrl.Result{Requeue: true}, nil
}
} else {
reqLogger.Info("got deletion request", "cave", cave.Name)
// Cave is being deleted:
// the deletion should be pending until backend pods are terminated
if containsFinalizer(cave.GetFinalizers(), caveProtectionFinalizer) {
// List backend pods
var backendPods v1.PodList
if err := r.List(ctx, &backendPods, client.InNamespace(req.Namespace), client.MatchingLabels{"app": "anylearn-backend"}); err != nil {
reqLogger.Error(err, "unable to list backend pods")
return ctrl.Result{}, err
}
// Count pods using current cave
nbOccupants := 0
for _, pod := range backendPods {
for _, v := range pod.Spec.Volumes {
if v.Name == cave.Name && v.NFS.Server == cave.Status.ServiceIP {
nbOccupants++
reqLogger.Info("Pod still using cave", pod.Name, cave.Name)
}
}
}
// Remove finalizer if backend pods are gone so that cave deletion can be truely proceeded
if nbOccupants == 0 {
controllerutil.RemoveFinalizer(&cave, caveProtectionFinalizer)
if err := r.Update(ctx, &cave); err != nil {
reqLogger.Error(err, "unable to remove protection finalizer from cave resource", "cave", cave.Name)
return ctrl.Result{Requeue: true}, err
}
// End of reconcilation
return ctrl.Result{}, nil
} else {
// Still got some pods using this cave, requeue the reconcilation
return ctrl.Result{Requeue: true, RequeueAfter: 20 * time.Second}, nil
}
}
}

// Some reconcilation logic in after...

可以看到,我们在调谐Cave时,首先要确保当一个Cave未被请求删除时,即deletionTimestamp为空时,用来保护Cave不先于后端被删除的Finalizer要加装到Cave对象上(升级考虑,旧系统已部署的Cave未采用Finalizer,而更新升级时我们不希望卸载重装Cave)。再来,当Cave已被请求删除时,判断finalizers标签是否存在,如果不存在则说明该Cave删除前的清理工作已处理完成,可以正常删除,便就此跳出调谐,交由K8s接管真正的Cave删除工作。若finalizers仍存在,则说明上一次调谐过后,还存在有与此Cave对象有挂载关系的后端pod,那么这里再重新list出所有后端pods,并计算出与此Cave对象有挂载关系的后端pods的数量(具体条件在本系统中由挂载卷名称name和挂载属性nfs.server共同决定):若数量为零,则移除finalizers并跳出调谐,交由K8s接管Cave的删除;若数量不为零,则强制安排间隔一段时间后的下一次调谐,以期在稍后的调谐时后端可以被清理干净。

五、相关资料

踩坑Nvidia显卡驱动及CUDA

这周新机器和显卡逐渐到位,开始装显卡驱动和CUDA,对过程中遇到的一些问题做个总结。

Boot

显卡总共有4种:A100、RTX2080Ti、K80和K40(包括K40c和K40m两种,这里不做区分),系统是Ubuntu20.04。一上来直接尝试用apt装驱动,所有机器统一装了nvidia-driver-460-server,结果几乎所有机器都重启失败,只能重装系统。这里推测是apt里的n卡驱动会把Xorg桌面程序装上并带起来,但处理不善导致系统无法启动。重装后手动切了booting target到命令行,避免桌面GUI的启动:(相关链接

1
sudo systemctl set-default multi-user.target

A100

驱动>450,cuda>11
用apt带的nvidia-driver-460系列安装后有几率无法重启,原因未知。官网cuda安装包,按装440 cuda10.2会报错,报显卡驱动的安装错误,错误码256。cuda11以上的runfile安装完成后,提示可以通过/usr/local/cuda/bin/cuda-uninstaller进行卸载,但实际上这个卸载器并不存在。而10.2的runfile安装包是装了这个卸载程序的。cuda11安装完成后,需要进一步ln几个库才能正常使用:

1
2
3
4
cd /usr/local/cuda-11.4/targets/x86_64-linux/lib && \
ln -s libcusolver.so.11 libcusolver.so.10 && \
ln -s ../../../extras/CUPTI/lib64/libcupti.so libcupti.so && \
ln -s ../../../extras/CUPTI/lib64/libcupti.so.11.4 libcupti.so.11.0

K80

可以装440+cuda10.2也可以装470+cuda11.4,相对来说兼容性更好,直接通过cuda runfile安装即可,另需按cuda11安装后所需的ln操作。

K40c/K40m

用apt带的nvidia-driver-460系列安装后有几率无法重启,原因未知。用官网cuda runfile安装包会报错,同样是报显卡驱动的安装错误,错误码256。其实是470驱动不支持k40,k40最高只能到460(截至20210708)。按官方support matrix,460驱动也可以搭配cuda11.4,因此可以官网下载460纯驱动先进行安装,安装前要停掉ubuntu默认的Nouveau驱动:

1
2
3
4
5
6
vi /etc/modprobe.d/blacklist-nouveau.conf

blacklist nouveau
options nouveau modeset=0

update-initramfs -u

装完驱动后,再用cuda11.4的runfile装cuda,安装时去掉自带的470驱动安装,仅安装cuda。这样安装完后就是460驱动的k40c/k40m+cuda11.4了。但是这样装完,daemon-reload或重启之后,nvidia-smi会报driver/library version mismatch。可能还是需要装cuda11.2才能完美匹配。

Harbor新手村

简介

Harbor是云原生的开源的通用artifact registry,CNCF毕业项目。它主要支持docker images和OCI-compatible artifacts相关的镜像管理,以及针对K8s构件的Helm chart管理。其特色为面向生产的artifact治理、安全保障和审计管理。

功能介绍

Harbor官方总结的系统特性有五个方面:

  1. 私有化便捷部署
  2. 多租户
  3. 镜像治理
  4. 安全与合规
  5. 可扩展性

其中镜像治理安全合规是重中之重,覆盖了镜像一致性、磁盘占用增长、镜像部署策略等镜像管理相关的重要问题。

阅读全文

Flask同步启动子线程问题

原有需求

Flask提供一套后端RESTful API,另有一个入口负责直连TCP传输文件

原有逻辑

Flask通过自带的dev WSGI启动,同时通过子线程启动一个socketserver标准库的ThreadingTCPServer,称之为FileSocketServer,这个socket server依赖Flask后端的部分组件来处理文件存储和数据库读写。我们把所有业务逻辑去掉,Flask只保留一个根路径的接口,socketserver处理请求(流式)时只是简单地向一个文件写入REQ,用来确认它是否正常工作。

app0.pyview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from socketserver import StreamRequestHandler, ThreadingTCPServer
import threading

from flask import Flask


class DummySocketHandler(StreamRequestHandler):
def handle(self):
with open("socket.log", "a") as f:
f.write("REQ\n")


def init():
socket_server = ThreadingTCPServer(("0.0.0.0", 32470),
DummySocketHandler)
threading.Thread(target=socket_server.serve_forever).start()


app = Flask(__name__)
app.debug = True

@app.route('/')
def home():
print("Hey", flush=True)
return "Hey"


if __name__ == '__main__':
init()
app.run(host="0.0.0.0", port=8888)

另外准备了一个简单的脚本对socketserver尝试建立连接。

test_connect.pyview raw
1
2
3
4
5
6
7
8
9
10
import socket

if __name__ == '__main__':
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.connect(("0.0.0.0", 32470))
print("Connection OK")
except Exception as e:
print("Connection KO")
print(e)

阅读全文

读《围城》2——凡人故事

距上一篇读书笔记已有近百日。断断续续地读着书,每次拿起来竟也能不忘记前文故事,着实可叹其笔墨的厚重扎实。

鸿渐的一部分情爱很能引起共鸣:面对心爱之人——唐小姐——的热切、卑微和看不透爱情的那种朦胧感,最终因两人始终无法表达清楚爱意,加上主观上的年轻气盛和客观上的沟通障碍而遗憾落幕。或许每个人年轻时都曾切实拥有过。然而在他与苏小姐的那段纠葛中,我却恨他过于暧昧,优柔寡断,并时常同情苏小姐自以为的两情相悦,却注定终不得爱,击案啐骂方某辜负妾意而羞于承认郎之无情(虽然后文中苏小姐的表现败了她作为失败者的惹人怜惜)。现在看来,这两段穿插交错的故事颇有意思:苏小姐在前,始于误会;唐小姐在后,终于误会。

之后在赶往三闾大学的途中,由辛楣挑拨起的对同行孙小姐的照顾和回顾,终究在到达三闾大学后不如意的生活中发展壮大成了怜惜和保护欲,终究水到渠成,两人走到一起。当然,其中免不了要说孙小姐的刻意为之,少了苏小姐的急功近利,多了许多示弱和柔情。这部分也许之后另作笔记,也许就此作罢。

鸿渐即便作为主角,依旧是个凡人,免不了嬉笑、怒骂、惆怅、刻薄,也免不了被俗世间的藤条抽着被迫成熟。书中刻画的他绝不是君子,亦非小人,实如你我这样的普通人。普通人的故事读起来却最有滋味,因为你总能在其中找到一丝自己和身边人的影子,仿佛沉浸其中便又活了一个人生。

国内apt换源(清华tuna源)

以Ubuntu为例,apt源信息位于/etc/apt/sources.list

备份并清空现有源:

1
2
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
>/etc/apt/sources.list

写入tuna源:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse

# 预发布软件源,不建议启用
# deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse

读《围城》1——人之于水,水之于人

初捧精装的《围城》,莫名有一种厚重感。不单是精装书本身的厚重,也是作品的知名度和曾经略有耳闻的一二思想。其中尤以“城外的人想冲进去,城内的人想逃出来”最为如雷贯耳,于我也颇为触动。

头几段文字平实的叙述很快将我带入航海船员和乘客的视角,偶尔辅以华丽却稳重的环境描写,引人入胜。

短短几句对法国人生活作风的概述刚刚带给我些许共鸣和不禁讪笑,以为钱老会再着写笔墨讽刺高卢人的种种奇特言行时,轰然瞥见平实却发人深省的一句话,被其深深吸引:

这船,依仗人的机巧,载满人的扰攘,寄满人的希望,热闹地行着,每分钟把沾污了人气的一小方面水,还给那无情、无尽、无际的大海。

《围城》第1章钱钟书

由此,诸如“人类的发展反使人类走向灭亡”、“人脑的存在使人作为动物平白消耗了极大能量”、“缺了一个人地球也不会不转”、“大自然的自我修复能力可以在人类灭绝后把人类对环境的破坏慢慢消除”云云,在我脑中一一浮现。

我极不善于总结和串联知识、资讯、思想,却热爱在脑中铺开自己的思考。

以人作为本体,各方面的发展是人脑带来的必然。发展就是那艘船。我们从上古开始就可以比其他动物更善于制造和使用工具。有了工具的辅助,我们不一定非要强健的体魄即可果腹。这一点似乎打破了作为动物的弱肉强食的生存法则。相较于很多凶猛的动物,一个人本身的力量不够强,速度不够快,耐力不够好。然而凭借身外的工具,我们却能够轻易杀死它们,或单单是吃得比他们饱、活得比他们好。我们偶然却也是必然地发现了这一现象,因而创造了更多更强大的身外工具,杀死肉身更强的猛兽,或单单是活得更好。抛开严谨的数据,单以感性的思考,人类的工具们时刻都在或多或少地影响着大自然,虽并不一定都是破坏。农耕的发展改变着生态,狩猎、畜牧、养殖改变着物种结构,工业的发展改变着大气、水循环,等等。有人存在的大自然的每一寸空间因此变得嘈杂,不再那么自然。而人脑赋予人类的想象力和创造力又使我们不断期许更加美好便捷的生活,产生源源不断的欲望和随其而来的动力,重复着一轮又一轮的发展。

以大自然或地球作为本体,不考虑什么神之类的在控制我们的世界,它仿佛对人类的发展淡然处之,却对我们的一些肆意妄为做出激烈的回应。人类的每一丝进步都输入给大自然,却从不会立刻收到它正面或负面的答复,它还在那里,像是支撑着人类的生存,也像是俯视着人类的生存,不做任何好与不好的评价。然而,人类发展至今也已不难发现,发展对大自然的破坏性已逐步产生对人类的惩罚:气候变化、沙漠化、灾害等等。人类恍然,凡是终有因果,改变趁早,以图业报。而大自然其实只是对人类带来的输入产生输出,它并不懂善有善报,这是人类的情感或思想或道德,与它无关。它既然能够包容些许的恶而不给予激烈的报复,为何妄图它因些许的善就一定要普施恩泽呢?水终究是要还给大海的,不论这水是干净还是肮脏,大海还是大海,并不是白海或黑海。