modified | Wednesday 2 July 2025 |
---|
收件箱中的内容常被当做灵感笔记使用。同时也会随手记录文学笔记。收件箱常被当做随手白板使用。
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
-BEGIN OF DOCUMENT –
零成本搭建现代博客之加载速度优化篇
19 April 2020 9:44 | 2144 字 | 1465 次阅读
独立博客 • 个人博客 • 技术博客 • 加载速度优化 • Chrome Audits • webpagetest • 图片优化 • CDN • AMP • 广告加载优化
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
本文属于零成本搭建现代博客指南系列第四篇【加载速度优化篇】。
个人博客搭建好后,如何优化加载速度?我们都知道如果一个网页在2至3秒还没有加载出来的话,用户因为焦虑就会关闭这个网页了。
在《零成本搭建现代博客之SEO优化篇》这篇文章里我们知道网站加载速度属于SEO技术性优化很重要的一个方面。如何提高网站加载速度对获取流量来说是个很重要的环节。
通过一个对Google的爬取规律观察,Google爬虫对新站一天大约爬半个小时,网站响应越慢,爬虫爬的速度也越慢。所以从网站被收录的角度看,提高网站加载速度也是很有必要的。
先说结果,本博客优化之前,Google PageSpeed Insights在移动端的评分为76分:
经过以下的一些方法优化后,移动端的评分为95分:
Google搜索结果现在对移动端越来越重视了,用户在移动端上对网站加载速度的要求也越高,所以对移动端的加载速度优化要更重视一些。
加载速度评估
要想优化加载速度,我们首先要通过一些方法量化分析网站加载速度,这样才能更好的对加载速度进行优化。以下是常用的一些分析网站加载速度的工具。
Google PageSpeed Insights
PageSpeed是检测网站加载速度很重要的工具,它可以给予我们非常详细的优化点,从中可以发现拖慢网站的元凶。
Chrome Audits(Lighthouse)
Chrome浏览器自带的Audits除了能让我们查看网站的加载速度,还可以查看网站最佳实践、可用性、SEO及PWA方面的评分及改进点。如果你的网站在这几方面评分都超过90分,那一定是Google在这几方面很喜欢的网站了。
webpagetest
webpagetest是非常强大的测试网站加载速度并深入分析网站资源加载速度的工具,还可以测试网站在不加载(屏蔽)某些资源的情况下网站加载速度与正常加载的对比,这样可以发现让网站加载速度变慢的资源或库。
我在测试本站的时候曾保留了一份测试报告,最终通过这个工具定位到了让网站评分降低的原因。
用户反馈
有时候不同地区的加载速度是不同的,更麻烦的是你很难知道当地的网络情况,这时候就需要用户的反馈了。
加载速度优化
图片优化
图片大小优化
在这篇《使用AWS Lambda提高网站图片加载速度1X倍》,我使用了Webp这种新型的图片格式让网站在Chrome下图片加载速度提高了十倍多。
优化图片大小非常重要,网站资源大头一般都是图片,图片太大会导致加载速度很慢,在手机端也很耗费用户的流量。一般可以通过对图片大小进行裁剪,对图片格式进行转换这些方式来降低图片大小。
图片懒加载
在这篇《Lazy load offscreen images with lazysizes》中提到了使用图片懒加载来延迟图片加载时机。当用户浏览到某个区域的时候该区域的图片才会开始加载,并不会一次加载网页全部的图片,lazysizes是一个对SEO友好的图片懒加载库,本站使用它实现图片懒加载。
前端资源优化
网站的前端资源越来越大了,尤其是当你使用了很多第三方库的时候。
用库一时爽,优化火葬场!
如果想极大的提高网站加载速度,尽可能使用少的第三方库。
JS和CSS优化
在这篇《Eliminate render-blocking resources》中提到了如何识别非重要的CSS和JS代码,通过去除不需要的代码减小前端资源文件大小。
第三方库优化
在这篇《Loading Third-Party JavaScript》中提到了如何正确加载第三方包,通过分析第三方包的加载时序图来判断对加载速度影响最大的几个因素,从而帮助你优化第三方包的加载。
内容结构优化
加载更多
一般对于瀑布流布局的网站,通过加载更多来降低每页返回的页面大小。不过这种方式对爬虫并不算友好,只有第一个页面可以被爬取到。
分页
常规网站一般通过分页来控制每页返回的页面大小。分页页面通过设置唯一的 canonical url 可以让爬虫更好的爬取到分页页面。
CDN
本站使用了免费的 netlify 作为CDN来提高网站在不同地区的加载速度,CDN通过边缘服务器缓存来提高用户请求网页的速度,一般的CDN都会设置某个失效时间后自动去源站拉取最新的内容缓存到边缘服务器。
你也可以选择诸如 Amazon CloudFront 与 cloudflare 这类CDN。还有一些特殊的比如具备反爬虫的CDN,如imperva收购的 distilnetworks 就是很厉害的反爬虫CDN。
静态化技术
动态网站一般需要做一些查询数据库和页面渲染的额外工作,为了提高网站响应速度,一些框架可以自动生成静态化页面部署到CDN中来提高网站响应速度。比如本站使用了基于 Hugo 的静态化技术框架,类似的还有 Hexo,这类技术都属于 JAMSTACK,还有新型的 gatsbyjs 技术让我们更容易开发出更快的网站。
AMP
AMP是Google推出的Web组件框架,可以加速移动端的访问速度,比如你可以看看本篇文章的AMP版本页面,在移动端访问速度是非常快的。
广告优化
使用webpagetest研究本站前端资源的加载速度,最终发现让网站变慢的是Google Adsense广告资源。
通过将广告延迟5秒展示加载的方式来提高页面访问速度,具体代码见 seo improve by delay google ads load,此举将网站加载速度评分提升至90+了。但是测试后发现这种延迟广告展示的方式会降低广告收入😂。
总结
在SEO技术性优化中,提升加载速度是很重要的一个方面,我们需要使用多种手段去降低页面返回的大小来提升网站的加载速度。而且在设计、开发与运营网站的全流程中,加载速度始终是各个环节都需要考虑的问题。本篇文章介绍的只是几个很简单的方面去提升,如果你有更好的方式,请留言交流,共同进步。
更新时间: 29个月前 版本: 6c7670696
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
BMPIGitHubTwitter学习技术投资|English
Serverless应用开发小记
30 August 2020 20:15 | 4456 字 | 7769 次阅读
什么是X
AWS • Lambda • Serverless • Terraform • Infrastructure as code • 无服务器架构 • Docker • AWS ECS • DevOps
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
背景
本文描述了笔者开发一个基于AWS Serverless技术的应用的全过程。基础设施使用Serverless Framework和Terraform搭建。系统的核心模块是每天都会执行一次的定时任务:该任务通过 Tushare 获取一组ETF指数基金价格数据,处理之后会将交易信号生成文本存放至S3桶。之后会发送消息给AWS SNS Topic ,订阅该主题的用户会收到邮件提醒。在系统的Web页面里有用户邮件订阅入口,也可以访问每天的历史交易信号记录。
本文覆盖以下内容:
构建一个Docker镜像来执行Core Service。
使用Terraform构建Core Service所需的以下AWS基础设施服务:
设置ECR仓库。
使用Fargate设置ECS集群。
设置Fargate任务。
设置CloudWatch定时任务。
设置IAM权限角色。
设置SNS主题。
设置VPC网络。
使用Serverless Framework构建API Service和Web所需的以下AWS基础设施服务:
设置Lambda函数。
设置API Gateway。
设置Route53。
设置CloudFront。
设置TLS证书。
设置S3桶。
设置CloudFormation。
最终效果见:线上版本。
源码见:代码仓库。
技术栈如下:
先决条件
你需要先注册以下账号:
AWS账号
Serverless账号
背景知识
应用架构演进历史1
单体应用(Monolithic):适合创业公司的小型应用;性能好。
面向服务(SOA):适合复杂企业业务的大型应用。
微服务(Microservices):复杂弹性可伸缩应用,适合有经验的团队。
无服务器(Serverless):成本低,适合后台任务;也适用于客户量大的应用程序和快速增长并需要无限扩展的应用程序。
Serverless
Build and run applications without thinking about servers2
无服务器计算(或简称serverless),是一种执行模型,在该模型中,云服务商(AWS,Azure或Google Cloud)负责通过动态分配资源来执行一段代码,并且仅收取运行代码所使用资源的费用。该代码通常运行在无状态的容器中,能够被包括HTTP请求、数据库事件、队列服务、监控报警、文件上传、调度事件(cron任务)等各种事件触发。被发送到云服务商执行的代码通常是以函数的形式,因此,无服务器计算有时是指 “函数即服务” 或者FAAS。3
优势
无服务器管理成本
弹性扩容
为服务运行时间付费
自带高可用、容错
劣势
冷启动性能差
监控与调试复杂
依赖云厂商
DevOps
这是DevOps工具全景图,我在此应用开发过程中也只用了一小部分工具,甚至没有测试流程。当然应用这些工具也需要考虑项目的实际情况,灵活应用。
应用架构
本应用分为三大模块:
Core Service:后台定时任务,提供获取基金价格数据、分析生成交易信号及发送邮件功能。
API Service:提供订阅主题API。
Web:提供订阅主题入口页面及查看历史交易信号记录功能。
实现
由于接下来需要使用多种AWS云服务,要了解这些服务请看这篇《AWS各服务解释》。
工程目录结构
.
├── api # api service
│ ├── serverless.yml
│ └── sns.js # lambda function
├── core # core service
│ ├── Dockerfile
│ ├── Infrastructure
│ │ └── tf-fargate
│ │ ├── cloudwatch.tf
│ │ ├── ecr.tf
│ │ ├── ecs.tf
│ │ ├── iam.tf
│ │ ├── main.tf
│ │ ├── output.tf
│ │ ├── sns.tf
│ │ ├── tasks
│ │ │ └── task_definition.json
│ │ ├── variables.tf
│ │ └── vpc.tf
│ ├── Makefile # CLI entry
│ ├── requirements.txt
│ └── src
│ ├── fund.txt
│ └── main.py # fargate task
└── web # web service
├── binaryMimeTypes.js
├── client
│ ├── assets
│ │ └── styles
│ │ └── global.less
│ ├── components
│ │ └── navbar.vue
│ ├── layouts
│ │ └── default.vue
│ ├── pages
│ │ └── index.vue
│ └── plugins
│ └── iview.js
├── index.js
├── nuxt.config.js
├── nuxt.js # lambda function
├── package-lock.json
├── package.json
├── secrets_example.json
├── serverless.yml
└── yarn.lock
Core Service
Core Service通过AWS Fargate运行,Fargate比Lambda更适合运行长时间的后台任务。Core Service用Python开发,要使其运行在AWS ECS环境中,先要构建Docker镜像,之后推送至AWS ECR仓库。
Docker镜像
FROM python:3.8-slim-buster
USER root
WORKDIR /tmp
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
RUN apt-get update
&& apt-get install -y wget
&& apt-get install -y build-essential
&& rm -rf /var/lib/apt/lists/*
RUN pip install numpy &&
wget http://prdownloads.sourceforge… &&
tar -xvzf ta-lib-0.4.0-src.tar.gz &&
cd ta-lib/ &&
./configure –prefix=/usr &&
make &&
make install
RUN rm -R ta-lib ta-lib-0.4.0-src.tar.gz
WORKDIR /code
COPY requirements.txt .
RUN pip3 install -r requirements.txt
COPY src/ .
CMD [ “python”, “./main.py” ]
在这里需要注意的是基础镜像的选择,一般我们在选择Python镜像的时候用的是alpine版本。但是alpine版本在安装一些本地二进制包的时候需要大量的编译,这个编译会遇到各种错误,最终我选择了buster版本,也就是基于ubuntu的版本。如果想进一步了解可看这篇《Using Alpine can make Python Docker builds 50× slower》。
需要先获取Tushare的API Token后,在本地运行Docker:
docker build -t invest-alchemy/core . # 构建
docker run -t -i -e TUSHARE_API_TOKEN=xxxx invest-alchemy/core # 本地运行
之后在AWS中创建ECR仓库,之后可以将本地构建的镜像推送到ECR中供ECS任务使用:
aws ecr get-login-password –region us-east-1 | docker login –username AWS –password-stdin replace_with_your_ecr_addr.dkr.ecr.us-east-1.amazonaws.com # 登陆ECR
docker build -t invest-alchemy/core . # 本地构建
docker tag invest-alchemy/core:latest replace_with_your_ecr_addr.dkr.ecr.us-east-1.amazonaws.com/invest-alchemy/core:latest # 打Tag
docker push replace_with_your_ecr_addr.dkr.ecr.us-east-1.amazonaws.com/invest-alchemy/core:latest # 推送远程仓库
构建基础设施
接下来通过Terraform构建所需的基础设施(ECS/IAM/SNS/VPC/CloudWatch)。这块主要参考这篇《Serverless job scheduling using AWS Fargate》。
ECR/ECS/Task
详见源码,此块不再赘述。注意将 capacity_provider 设置为 FARGATE_SPOT 可大幅降低成本。
CloudWatch
详见源码,此块不再赘述。注意在ecs_target/network_configuration设置可使用默认的VPC网络,一定要将 assign_public_ip 设置为 true 才行,否则容器任务将无法访问外部网络。
VPC
使用AWS默认的VPC网络。AWS Fargate可运行在多种网络模式下,这里选择了最简单的公共子网模式。
其他模式见《CloudFormation Templates for AWS Fargate deployments》。
要想进一步学习可看这篇《Fargate networking 101》。
IAM
######################### Role used by the container regulates what AWS services the task has access to, e.g. your application is using a DynamoDB, then the task role must give the task access to Dynamo.
resource “aws_iam_role” “ecs_service_role” {
name = “\({var.project}_ecs_service_role_\)
{var.env}”
assume_role_policy = “${data.aws_iam_policy_document.ecs_service_assume_role_policy.json}”
}
resource “aws_iam_role_policy” “ecs_service_policy” {
name = “\({var.project}_ecs_service_role_policy_\)
{var.env}”
policy = “\({data.aws_iam_policy_document.ecs_service_policy.json}"
role = "\)
{aws_iam_role.ecs_service_role.id}”
}
data “aws_iam_policy_document” “ecs_service_policy” {
statement {
effect = “Allow”
resources = ["*"]
actions = [
“iam:ListPolicies”,
“iam:GetPolicyVersion”
]
}
}
data “aws_iam_policy_document” “ecs_service_assume_role_policy” {
statement {
actions = [“sts:AssumeRole”]
principals {
type = “Service”
identifiers = [“ecs-tasks.amazonaws.com”]
}
}
}
resource “aws_iam_role_policy_attachment” “ecs_service_role_policy_attachment” {
role = aws_iam_role.ecs_service_role.name
policy_arn = “arn:aws:iam::aws:policy/AWSLambdaFullAccess” # https://gist.github.com/gene1w…
}
######################### Role used by the container enables the service to e.g. pull the image from ECR, spin up or deregister tasks etc
resource “aws_iam_role” “ecs_task_execution_role” {
name = “\({var.project}_ecs_task_execution_role_\)
{var.env}”
assume_role_policy = «EOF
{
“Version”: “ 17 October 2012 ”,
“Statement”: [
{
“Action”: “sts:AssumeRole”,
“Principal”: {
“Service”: “ecs-tasks.amazonaws.com”
},
“Effect”: “Allow”,
“Sid”: ""
}
]
}
EOF
}
resource “aws_iam_role_policy_attachment” “ecs_task_execution_role_policy_attachment” {
role = aws_iam_role.ecs_task_execution_role.name
policy_arn = “arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy” # https://gist.github.com/gene1w…
}
######################### Role used for ECS Events
resource “aws_iam_role” “ecs_events_role” {
name = “\({var.project}_ecs_events_role_\)
{var.env}”
assume_role_policy = “${data.aws_iam_policy_document.ecs_events_assume_role_policy.json}”
}
resource “aws_iam_role_policy_attachment” “ecs_events_role_policy” {
policy_arn = “\({data.aws_iam_policy.ecs_events_policy.arn}"
role = "\)
{aws_iam_role.ecs_events_role.id}”
}
data “aws_iam_policy” “ecs_events_policy” {
arn = “arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole” # https://gist.github.com/gene1w…
}
data “aws_iam_policy_document” “ecs_events_assume_role_policy” {
statement {
actions = [“sts:AssumeRole”]
principals {
type = “Service”
identifiers = [“events.amazonaws.com”]
}
}
}
这里定义了三个角色:
ecs_service_role:容器应用权限,比如Core Service需要给S3和SNS推送数据,就需要此角色具备相关权限。
ecs_task_execution_role:ECS任务执行权限,比如ECS需要从ECR拉取镜像,需要具备访问ECR的权限。
ecs_events_role:CloudWatch定时任务权限,比如定时任务需要执行ECS任务,需具备 AmazonEC2ContainerServiceEventsRole 角色的权限。
敏感信息
系统总是需要依赖一些敏感信息,比如各类api token 。Core Service依赖Tushare API token,所以需将其通过Terraform注入。在这里我参考了这篇《A comprehensive guide to managing secrets in your Terraform code》。
文章提供了这几种管理敏感信息的方式:
环境变量
加密文件(AWS KMS)
密钥仓库(AWS Secrets manager)
第一种环境变量方式最简单,之后两种有一定的使用成本,因为对密钥安全要求不高,这里采用了第一种简单的方式。
先定义变量:
variable “TUSHARE_API_TOKEN” {
description = “Tushare API Token from .env”
type = string
}
然后在ECS定义处注入此环境变量:
data “template_file” “task” {
template = “${file(”./Infrastructure/tf-fargate/tasks/task_definition.json")}"
vars = {
project = “\({var.project}"
aws_region = "\)
{var.aws_region}”
ecr_image_uri = “\({var.ecr_image_uri}"
TUSHARE_API_TOKEN = "\)
{var.TUSHARE_API_TOKEN}” # 注入变量
}
}
最后在 task_definition.json 处将此变量注入容器:
“environment”: [{“name”: “TUSHARE_API_TOKEN”, “value”: “${TUSHARE_API_TOKEN}”}]
每次执行变更时需要输入这个key。这样就不会将key泄露至代码仓库了。
Make构建脚本
################ Config ########################
S3_BUCKET ?= invest-alchemy
AWS_REGION ?= us-east-1
ENV ?= dev
ECR := 745121664662.dkr.ecr.us-east-1.amazonaws.com/invest-alchemy-core-ecr-dev # ECR Repository Example: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/{project_name}-ecr-{env}
################################################
################ Artifacts Bucket ##############
artifacts:
@echo “Creation of artifacts bucket”
@aws s3 mb s3://\((S3_BUCKET)
@aws s3api put-bucket-encryption --bucket \)
(S3_BUCKET)
–server-side-encryption-configuration
‘{“Rules”: [{“ApplyServerSideEncryptionByDefault”: {“SSEAlgorithm”: “AES256”}}]}’
@aws s3api put-bucket-versioning –bucket $(S3_BUCKET) –versioning-configuration Status=Enabled
################################################
build-docker:
@echo “run aws ecr get-login –region \((AWS_REGION) first"
@docker build -t \)
(PROJECT) .
@docker tag \((PROJECT) \)
(ECR)
@docker push $(ECR)
################ Terraform #####################
init:
@. .env
@terraform init
-backend-config=“bucket=\((S3_BUCKET)" \
-backend-config="key=\)
(PROJECT)/terraform.tfstate”
./Infrastructure/tf-fargate/
validate:
@terraform validate ./Infrastructure/tf-fargate/
plan:
@terraform plan
-var=“env=\((ENV)" \
-var="project=\)
(PROJECT)”
-var=“description=\((DESCRIPTION)" \
-var="aws_region=\)
(AWS_REGION)”
-var=“artifacts_bucket=$(S3_BUCKET)”
./Infrastructure/tf-fargate/
apply:
@terraform apply
-var=“env=\((ENV)" \
-var="project=\)
(PROJECT)”
-var=“description=$(DESCRIPTION)”
-compact-warnings ./Infrastructure/tf-fargate/
destroy:
@read -p “Are you sure that you want to destroy: ‘\((PROJECT)-\)
(ENV)-$(AWS_REGION)’? [yes/N]: " sure && [ $${sure:-N} = ‘yes’ ]
@terraform destroy ./Infrastructure/tf-fargate/
先执行 make build-docker 构建镜像并上传至ECR,然后执行 make init 初始化Terraform。之后执行 make validate && make plan 验证下基础设施配置有无问题。如果都没有问题则执行 make apply 构建真正的基础设施。
工作流
如果系统代码有改动,则可重复执行以下流程:
修改代码
make build-docker
make apply
API Service
API Service只有一个订阅SNS主题的API,用来帮助用户订阅Core Service提供的主题。
Serverless Framework
service: api
app: invest-alchemy
org: madawei2699
provider:
name: aws
role: arn:aws:iam::745121664662:role/invest-alchemy-lambda
runtime: nodejs12.x
stage: dev
region: us-east-1
functions:
subscribe_sns:
handler: sns.subscribe_sns
memorySize: 128
description: Subscribe sns topic.
environment:
snsTopicArn: arn:aws:sns:us-east-1:745121664662:trade-signal-topic
events:
- http:
path: subscribe
method: post
cors: true
integration: LAMBDA
此处定义了subscribe_sns的函数,我们用Javascript实现,源码详见 sns.js,此处不再赘述。
需要注意的是代码内需要定义SNS主题,所以需要具备订阅SNS的角色权限,这个我们在Serverless配置中指定了此函数执行的Role为 arn:aws:iam::745121664662:role/invest-alchemy-lambda,该角色具备订阅SNS的权限,它附加的策略有 AWSLambdaBasicExecutionRole 和 AmazonSNSFullAccess。
在Lambda中执行的代码可以直接导入AWS SDK而无需安装,同时也无需设置AWS Credentials,因为函数执行时直接具备角色附带的权限。
Web
Web系统提供了用户可访问的网页以供用户注册订阅邮箱。在该模块采用了Vue.js和Nuxt.js构建SEO友好的服务端渲染(Server-Side Rendered)网页。我主要参考这篇《AWS | Vue Nuxt Ssr》。
service: web
app: invest-alchemy
org: madawei2699
provider:
name: aws
runtime: nodejs12.x
stage: \({self:custom.secrets.NODE_ENV}
region: us-east-1
environment:
NODE_ENV: \)
{self:custom.secrets.NODE_ENV}
functions:
nuxt:
handler: index.nuxt
memorySize: 256
events:
- http:
path: /
method: any
cors: true
- http:
path: /{proxy+}
method: any
cors: true
plugins:
custom:
secrets: \({file(secrets.json)}
apigwBinary:
types:
- '*/*'
customDomain:
domainName: \)
{self:custom.secrets.DOMAIN}
basePath: ’’
stage: ${self:custom.secrets.NODE_ENV}
createRoute53Record: true
这里需要注意的是网关集成了自定义域名和TLS证书:需要先在AWS Route53配置域名,然后在AWS Certificate Manager申请TLS证书,具体过程请参考上面提到的文章。
等证书申请成功后就可以执行 sls create_domain 创建域名的DNS信息。
最后执行 npm run deploy 部署至AWS 。如果想本地调试可以执行 npm run start-server。
调试/日志
在AWS控制台中选择CloudWatch服务查看日志组,通过分析相关的日志定位问题。如果没有日志生成,也可以通过查看 CloudTrail 中的日志查看定时任务事件是否生成。
需要注意的问题
Serverless成本计算
Serverless的一大优势是按需付费,对于一些定时任务或流量小的网站,成本比单独购买VPS要便宜。另外还具备了极高的可用性与弹性扩容能力,这也是单台VPS不可能做到的。
想分析花费可以通过AWS Billing服务。我们所用的收费服务(排除了一些在该系统收费可忽略的服务如S3/VPC/CloudFront等)包括:
API Gateway:每1百万次请求1美元。
ECS Spot Fargate:每vCPU/时为0.01289974美元,每GB内存/时为0.00141649美元。
Lambda:每128M内存/100毫秒为0.0000002083美元。
SNS:每10万次Email推送2美元。
以上都以US East区计算。
从CloudWatch日志组中得知每次运行API与Web服务的Lambda每个请求执行时间为1200 ms;Core服务每天运行3分钟计算。一个月流量假设有10万PV,每PV分别产生一次Web与API请求。Lambda的费用为:200000 * 1200 / 100 * 0.0000002083 = 0.5美元;ECS费用为3 * 30 / 60 * (0.01289974 + 0.00141649) = 0.02美元。API Gateway费用为200000 / 1000000 = 0.2美元;SNS假设有1千人订阅邮件,则为1000 * 30 / 100000 = 0.3美元。
则每月成本为:0.5 + 0.02 + 0.2 + 0.3 = 1.02美元。以 vultr VPS 为例,最便宜的配置为1 vCPU + 512 MB配置一个月为2.5美元,这还不包括发邮件的成本。
冷启动慢
后台定时任务对启动时间不敏感,如果非常在意启动时间可通过一些定时任务不断的预热,不过这也会导致成本提高。想进一步了解可参考这篇《Solving Serverless Cold Starts with Advanced Tooling》。
VPC收费坑
VPC大部分服务不收费,除了VPN/NAT Gateway/Endpoints(Gateway免费/Interface收费),而Interface的Endpoints收费很贵,不需要的时候尽可能停止掉,而要想单独删除还不行,必须把相关AWS的服务删除掉才能停止收费。
需要注意的是,Endpoints是按照可用区域收费的,创建一个Endpoints如果存在6个可用区域,那就是按6倍来计费的。
阅读材料
Serverless Stack
Serverless应用开发指南
Serverless好文
References
https://rubygarage.org/blog/mo… ↩︎
https://aws.amazon.com/serverl… ↩︎
https://serverless-stack.com/c… ↩︎
BMPIGitHubTwitter学习技术投资|English
基于Serverless实现静态博客访问统计功能
24 January 2021 6:18 | 1386 字 | 5526 次阅读
AWS • Lambda • Serverless • Pulumi • Vercel • Infrastructure as code • 无服务器架构 • Hugo • 独立博客 • 个人博客 • 技术博客
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
背景
我在 零成本搭建现代博客指南 系列文章里详细介绍了本博客的搭建过程,本站是基于Hugo框架生成的,并部署在Vercel平台上。
和最流行的CMS框架Wordpress不同,Hugo/Hexo/Gatsby等静态站点生成的技术好处在于:
成本极低甚至免费,无需专门的VPS服务器,只需要将生成好的静态页面托管到CDN/GitHub Pages/Vercel/Netlify/AWS S3这类平台上即可;
Wordpress并发性能差,而静态博客只受限于与流量带宽的限制,不存在性能和并发问题。
但和Wordpress相比,因为全站都是静态化生成的页面,没有后台服务器的接口支持,无法实现一些很简单的带状态的功能,如不支持页面阅读数量统计功能。
这种 Jamstack 架构的站点要实现动态的功能,一般会采用Serverless的方式去实现。
为何要实现博客的访问统计功能
本博客早已接入了Google Analytics ,但是为何还要实现博客的访问统计功能?一方面考虑到文章的阅读数量可以供读者更容易识别此篇文章是否是热门文章,另一方面这种阅读量数据对百度SEO有正向提升作用。Google Analytics的问题在于一旦用户安装了某些屏蔽广告插件后,同时也会导致GA无法正常工作,根据某些文章统计说一般会损失近10%的流量统计。
实现访问统计功能的技术选择
在选择实现此功能的过程中,我有如下选择:
使用Vercel/Netlify提供的 Serverless Functions,这种方式可以参考这篇 Building a Modern-Day Hit Counter ;
使用Next.js框架,这种方式可以参考这篇 Real-Time Blog Post Views With Next.js and Firebase ;
使用基于AWS的Serverless架构实现。
因为之前使用过Terraform实现基于AWS平台的Serverless架构,具体过程可以参加这篇 Serverless应用开发小记 ,所以我最终选择了第三种方式去做。
这次我使用Pulumi实现Serverless架构。Pulumi相比Terraform的优势在于可以以编程方式实现Infrastructure as code ,相信看完该篇文章你就能知道它的强大了。
使用Pulumi实现博客访问统计接口
先创建一个 Pulumi 工程:
我们选AWS + TypeScript 。
之后按提示创建目录及安装依赖,工程目录如下:
.
├── Pulumi.dev.yaml
├── Pulumi.yaml
├── README.md
├── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json
只需要编辑index.ts即可,代码如下:
import * as aws from “@pulumi/aws”;
import * as awsx from “@pulumi/awsx”;
import * as pulumi from “@pulumi/pulumi”;
// Create a mapping from ‘route’ to a count
const counterTable = new aws.dynamodb.Table(“api-bmpi-dev”, {
attributes: [{
name: “id”,
type: “S”,
}],
hashKey: “id”,
readCapacity: 5,
writeCapacity: 5,
});
// let bmpiAllPageViewsRoute = “bmpi-dev-all-page-views”; // all page views key
// Create an views API endpoint
const viewsEndpoint = new awsx.apigateway.API(“bmpi-dev-post-views”, {
routes: [{
path: “/{route+}”,
method: “GET”,
eventHandler: async (event) => {
const route = event.pathParameters![“route”];
console.log(`Getting count for '${route}'`);
const client = new aws.sdk.DynamoDB.DocumentClient();
const result = await client.update({
TableName: counterTable.name.get(),
Key: { id: route },
UpdateExpression: "SET hit = if_not_exists(hit, :zero) + :incr",
ExpressionAttributeValues: { ":zero": 0, ":incr": 1 },
ReturnValues:"UPDATED_NEW",
}).promise();
let count = result.Attributes!.hit;
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Origin": "https://www.bmpi.dev",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET"
},
body: JSON.stringify({ route, count }),
};
},
}],
});
// We have registered a domain, and a cert already (we could do most of this with Pulumi as well if we wanted!)
let domainName = “api.bmpi.dev”; // bmpi.dev
is regitered with Route53
let route53DomainZoneId = “Z3D746J1ZFXBYQ”; // The Hosted Zone I got when I registered bmpi.dev
let certARN = “arn:aws:acm:us-east-1:745121664662:certificate/6e610430-46c8-4884-a1f7-471be45decf4”; // ACM cert for *.bmpi.dev
// API Gateway requires we register the Domain with it first
const domain = new aws.apigateway.DomainName(“domain”, {
certificateArn: certARN,
domainName: domainName,
});
// Then we can map a REST API to a domain with a BasePathMapping
const mapping = new aws.apigateway.BasePathMapping(“mapping”, {
restApi: viewsEndpoint.restAPI,
basePath: “page-views”, // We map our API into the “/page-views” base path
stageName: viewsEndpoint.stage.stageName, // We map the stage we got for free with .x.API
above
domainName: domain.domainName, // We map it into the domain we registered above
});
// Finally, we need a DNS reocrd to point at our API Gateway
const record = new aws.route53.Record(“record”, {
type: “A”,
zoneId: route53DomainZoneId,
name: domainName, // Write a record for api.bmpi.dev
into the zone for bmpi.dev
aliases: [{
name: domain.cloudfrontDomainName, // APIGateway provides it’s own CloudFront distribution we can point at…
zoneId: domain.cloudfrontZoneId,
evaluateTargetHealth: true,
}],
});
// It might take a while after we deploy the record above before the DNS propagates and allows us to resolve these URLs…
export let endpoint = pulumi.interpolatehttps://${record.name}/page-views
;
所有的代码就这些,然后一键部署上线:
pulumi up
不到100行的代码我们完成了什么功能?
创建一个dynamodb表;
实现了一个Serverless API,支持以原子性更新特定URL的访问计数,并且开启了 CORS 支持;
创建了一个API Gateway并且绑定了 api.bmpi.dev 域名且开启了HTTPS支持;
创建了 api.bmpi.dev 域名的DNS记录;
输出了此接口的请求地址。
体会到Pulumi的强大了吗?一行代码完成了很多基础设施的配置,这就是IaC的强大!
之后的工作就简单了,我们只需要在页面通过JS调用此接口即可,代码见这个commit: add post page hit count feature 。
最终效果见:
总结
Jamstack + Serverless是实现现代Web开发很流行的方式,尤其是在做一些个人项目的时候,一个人就是一个团队。
作为一名软件工程师,我很喜欢去研究某个产品或公司背后的技术栈,从产品外在的蛛丝马迹去窥探背后所用的技术栈是一件很有趣的事情。了解不同产品所用的技术栈不仅有助于我开阔眼界,也能提高我个人产品或项目的产出效率,比如发现新的有趣的技术或工具我都会尝试将其融入到我的个人技术栈中。
个人项目追求的是短平快,所选的技术栈和公司项目差异很大。比如很多公司项目的主力编程语言为Java,Java属于很适合大规模协作的工业级语言,优秀的工程化能力使其很适合团队协作。不过它并不适合个人项目使用,有些刻板的语法和笨重的JVM导致其很难运行在低内存的服务器上。
回顾我的一些 个人产品或项目,所用的技术栈本身也在不断的演进,我始终追求的是使用更高效的工具完成高质量的技术设计与实现。当然技术只是工具,我只是在选择适合的工具去实现我的产品或项目目标。
在看到这篇 The Tech Stack of a One-Man SaaS 文章时,我产生了写作这篇文章的想法。在接下来的内容中我会将自己所用的技术或工具分享并做简短的点评,这些点评只是我个人的主观感受,某些观点可能存在片面的认知,如果你有不同的想法,欢迎留言与我沟通讨论。
编程语言
编程语言的选择不只反应了一个程序员的品味和追求,同时也会影响他/她的个人思维模式和产出效率。
编程语言的选择可不只是换一种语法,不同的语言具备不同的特点。比如你用PHP开发了一个不错的产品,突然流量大爆发,并发问题很快就出现了。如果大量的添加服务器不仅会在产品收益不明确的情况下增加投资风险,同时也增加了维护成本。个人项目本身就是利用业余时间做的,资金和时间是很有限的,如何在有限的时间内产生最大的收益,编程语言的选择是个重要的影响因素。
除了并发这种一般产品不会遇到的问题,另外一个对开发效率产生重大影响的莫过于语言的表达力,同样的功能,强表达力的语言可以用很少的代码完成,代码量越少出现Bug的机会也越少。比如这篇 如何阅读一份代码 文章中分析Rails (Ruby)、Django (Python)与Phoenix (Elixir)框架的总代码规模,Phoenix比前两个小了整整一个数量级,可见Elixir的表达力有多强。
表达力另外一个重要的影响在于会影响阅读代码。比如Clojure的 Liberator 是一个实现 HTTP/1.1 协议的RESTful库,总代码行数只有1.2K行,如果要研究HTTP协议的话读这个库的源码是个很好的途径。如果换成Java库的话,估计上万行只是一个开始。因为Elixir强大的表达力,其很多库总规模都在几千行之下,这种规模不会让人望而却步。使用这类语言会在不知不觉中,让你被动的阅读了很多优秀的源码,也逐渐提高了使用者的编码与设计能力。
Elixir — 我的主力后端语言。继承Erlang分布式、高并发与容错的能力,有着如Ruby般简洁而强大的表达力,还能 以REPL驱动开发,这是一门非常适合个人项目的编程语言。我曾用它花费一周业余时间开发出一个峰值日PV达3万的 网站,考虑到网站的爬虫请求,日请求达10万,仅仅需要一台内存1GB单核CPU的月收费仅5美元的VPS服务器。
Python — 万能的胶水语言。我曾用它开发过很多自动化或提升效率的小工具,也用它开发过爬虫、流量站、 App和小程序的后台系统,还用它分析过股票交易的一些数据,可以说我的生活离不开Python。
Clojure — 可能是JVM生态里最适合个人项目的编程语言了。无论是Java/Kotlin/Scala都不小巧。Clojure是一种Lisp方言,拥有很多Lisp的优点,内核小巧但具备极强的扩展能力。同时能利用Java生态丰富的轮子,不用重复造轮子就是高生产力的体现。Clojure也是全栈,既可以做后端也可以做前端(ClojureScript,开源的类 Roam Research 的笔记工具 Logseq 就是使用它开发的)。我在个人项目中还没有使用过它,不过准备尝试在一些个人项目中使用。
JavaScript — Web开发离不开JavaScript,无论你爱不爱它,都离不开它。我的前端技术仅限于开发简单的网站。不过JavaScript的应用范围是越来越大了,除了在网页端的应用,很多跨平台的桌面端程序都通过 Electron 技术将JavaScript程序封装打包出的。随着 Serverless 技术的普及,JavaScript也成为Serverless开发第一支持的编程语言,不只可以做Function,也可以实现 IaC 构建基础设施(如 Pulumi 中支持用JavaScript描述云服务基础设施)。
Swift — 想在Apple生态赚钱离不开的编程语言。早先做iOS开发的时候,我用Swift开发上架过两个App。Swift当时ABI还没稳定,经常从入门到重新入门,语法很不稳定,不过其语言的表达力相比它的老大哥Objective-C还是强不少(曾经用Swift重构过一个App,同样的功能其比Objective-C要少4/5的代码)。如果未来还要在iOS上开发App,我还是会选择Swift。
Go - Go有着非常简单的语法,几天就可以上手,并且非常适合开发一些I/O密集型的应用。在JVM的世界中,如果你要开发一个高性能的Web应用,可能需要掌握很多底层的知识,但用Go却很容易实现相同甚至更高的并发吞吐量,这都得益于Go的编译器与运行时做了很多的优化。我现在会用Go去写一些个人项目所需的小工具。我曾经做过一些压力测试来对比Go与基于Nginx的Clojure的应用的并发性能,详见这个GitHub Issue,这也会让你对Go的简单与高并发有个直观的认知。
框架与库
好的框架,事半功倍。
Phoenix — Phoenix是我用过很好的Web MVC框架。其有着Rails的工程设计,同时具备Erlang平台的高并发容错能力。得益于Erlang的进程管理能力,Phoenix很擅长处理长链接,比如实时聊天或者游戏的应用场景。这篇 The Road to 2 Million Websocket Connections in Phoenix 文章分享了在Phoenix单机支持200万Websocket长链接的测试。
Django — Django是很适合做CMS管理系统的框架。我用它来做一些App或者 小程序的后台管理系统,其可以直接从数据库生成CRUD代码,甚至包括管理界面。
Laravel — PHP语言里最流行的MVC框架。很早的时候简单用过,不过考虑到性能始终是它的达摩克利斯之剑,我最终用Phoenix替代了它。
Pure CSS — 非常轻量级的响应式CSS库。我用它替代庞大的Bootstrap库,越轻量的库对加载速度的影响越小,而这会潜在的影响网站的用户体验和 SEO。
Vue.js — 用Vue写过一些简单的页面,个人感觉入门很快。适合后端开发者写前端应用。
NextJS — React框架很复杂,这框架是为了让使用React变得简单。我没有用过这个框架,不过这个框架很火,未来的Web应用我会尝试使用这个框架去开发。
Tailwind CSS — 写CSS一直让我很头疼,大概原因是CSS和正常的编程语言完全不同(并不是图灵完备),有很多难以预料的情况埋藏在冰山之下。这框架是让写CSS像写后端一样简单。听说 Tailwind + NextJS 配合起来效果更棒。未来我也会尝试用它来替代直接写CSS。
Tushare — 量化交易A股必备库。可以很方便的获取各种A股股票和基金行情数据。不过数据质量 Pro 版本的更高,需要一定的积分才能调用相关API。我的 双均线交易策略提醒策略模块 就是用此库开发的。
Scrapy — Python第一爬虫库。大多时候我都用 Requests 库解决刮取(Scrape)数据的问题,Scrapy更适合大规模的爬取网站场景。谨记 爬虫玩得好,牢饭吃到饱。
技术架构
单体应用 — 单体应用很适合个人项目,和流行微服务架构相反,个人项目追求用简单的架构去快速完成MVP版本。如果架构太复杂了,会导致开发与维护成本达到难以接受的程度。
微服务 — 我的个人项目没有微服务架构的,倒是公司项目用微服务架构的很多。因为微服务架构更容易将大的功能拆分成不同的微服务模块供不同团队协作开发,同时扩容做起来更容易一些。
Serverless — 随着Serverless的逐步成熟,我在最近的一些项目逐渐开始应用。比如本 博客的文章阅读统计 就是使用Serverless架构开发实现的。这种架构很适合流量比较低的应用,成本相比直接租用单台服务器便宜,哪怕流量起来了服务也很容易弹性扩容。因为个人项目的用户数量一般都比较低,尤其是刚开始流量可以忽略不计,该架构就很适合这种场景,同时可以利用很多云平台的服务完成复杂的功能开发。公司的项目可以配置全功能团队包括DevOps ,但个人项目的DevOps工作可以通过Serverless架构转移给云平台。
数据管理
核心是数据。
SQLite — SQLite是个 严肃的数据库,还是 真正的Serverless数据库,比如这个 datasette 库可以将SQLite数据库的数据直接发布成在线数据。甚至可以将 SQLite托管至Github Pages。我的一些小工具甚至静态化渲染的网站背后的数据库就是它。其还可以用作强大的数据分析平台。要说缺点的话,它的数据库不支持并发写入,这也是我在个人项目中不得不使用重量级的关系数据库。
MySQL — 公司项目和个人项目都会用的数据库。MySQL除了有点重量级外,对于个人项目来说没有啥缺点。
PostgreSQL — 相比MySQL来说,PostgreSQL支持很多高级特性。比如这个 postgrest 库可直接将PostgreSQL数据库发布成REST API,甚至有基于此库的SaaS服务如 supabase 可提供类 Google Firebase 的功能。
Redis — Redis本身的功能很丰富(缓存、计数、分布式锁与排行榜等),不过我在个人项目中主要用来做缓存。
AWS DynamoDB — AWS的NoSQL数据库。它的优势在于可以作为以AWS众多服务构建Serverless架构的数据库,比如本博客的阅读量数据就是存放在此服务上的。
AWS S3 — 价格便宜的无限存储服务。S3的玩法很多:
爬虫爬取的数据可以存放上去,比如 GH Archive 定时爬取GitHub各类事件然后存储至S3以供进一步做大数据分析;
定时生成的静态页面存放上去,我有一个网站就是通过定时程序生成几十万静态页面托管上去的;
将日志存放上去做进一步的分析,比如网站的请求日志存放上去通过 AWS Athena 以SQL形式查询做数据分析。
Google Sheets — Google Sheets不只是个具备实时协作的电子表格。它还可以:
可作为数据库。比如 Sheety 这个服务可将电子表格作为数据库并发布成API以供应用去消费数据;
可作为TMS系统。这个 Tweet 显示如何用Google Sheets做多国语言的翻译;
可作为CMS系统。比如结合AWS Lambda可做出一个 博客后台管理系统。比如我的 社交数据监控Dashboard 后台就是Google Sheets,结合 Apps Script 可以做到自动获取最新社交数据。
基础设施
个人项目的关键在自动化,自动化的重点在基础设施。
云平台
AWS — 除了有点贵,其他都很好的云平台。AWS的一些服务很便宜但某些非常贵,在全面应用个人项目时最好先踩坑测试了解下。对于个人开发者来说加入 AWS Community Builders 是个学习AWS不错的途径,有啥问题可以直接在社区请求支持。我在学习测试了小半年后将个人项目全面转向了AWS平台。
S3 — 无限存储。适合部署静态网站,费用便宜。还可以存放日志等各种文件。
CloudFront -访问速度并不快的CDN,结合S3服务可以直接部署静态网站。
SQS — 标准队列+ FIFO消息队列,如果个人项目中需要MQ的话可以考虑使用这个。
SNS — 推送服务,支持邮件、短信及App推送。我在 双均线交易策略提醒模块 就是使用此服务实现的。
EC2 — 弹性计算服务器。个人项目对实时性要求不高的服务或者一些后台任务可以选择Spot实例,比如我的一个开源项目中使用Spot实例购买的单核1GB内存的服务器一个月的花费只有不到3美刀。这比很多VPS服务商的价格都便宜。
Lambda — 按需收费的Serverless计算服务。比如本博客的 图片优化功能 就是使用此服务运行的。
Dynamodb — 见 数据管理 里的介绍。
CloudWatch — 可查看各服务的日志,调试Lambda必备。还可以做定时任务,最小分钟级别。
ECS — 运行Docker的服务。
ECR — 存放自定义Docker镜像的私有仓库。
Fargate — 适合任务型服务,冷启动时间超过一分钟,按秒计费,需结合ECS/EKS运行。比如我在 双均线交易策略任务模块 中使用此服务定时生成交易信号。
CloudFormation — AWS的IaC,基础设施代码化。比如我用这个 aws-ethereum-miner 模版去启动EC2集群挖矿。
Certificate Manager — HTTPS证书管理服务,我所有的域名TLS证书都用它免费申请的。
API Gateway — API网关,我一般用它做Serverless架构的REST API网关。
Billing — 时刻关注账单变化,可以设置某个阈值,超过立即发邮件告警。
其他服务 — 这里面有一些非常隐蔽收费很贵的服务,我就曾掉过坑里,比如Aurora Serverless、VPC等服务,具体可看我的这个 AWS各服务解释 的详细介绍。
Vercel — 非常好的静态网站部署服务,Vercel的CDN在国内访问速度很快。本博客与 我常用的工具 网站都是用其一键部署的。
Netlify — 静态网站部署服务,已被Vercel替代,因为其CDN在国内访问很慢。
Vultr — VPS服务,已被AWS替代,AWS EC2 Spot实例很便宜。
负载均衡
Nginx — HTTP应用与反向代理服务,也可做TCP/UDP代理服务。早年我搭建个人博客或静态网站都需要使用Nginx,不过现在静态网站我都使用Vercel或者AWS S3 + CloudFront的组合。当然很多复杂的系统还是得用Nginx或 OpenResty,OpenResty可以直接使用Lua脚本编写复杂的逻辑运行在Nginx之上,可以做很多好玩的应用,快速构造出足以胜任10K乃至1000K以上单机并发连接的高性能Web应用系统。如果对Lua脚本不熟悉,还可以使用 Nginx-Clojure,一个在Nginx进程上运行JVM虚拟机的框架,可以使用Clojure/Java/Groovy编程语言开发基于Nginx的高并发应用。
DevOps相关
GitHub Actions — GitHub提供的CI/CD服务。我在 我的时间管理工具 这篇文章分享过利用GitHub Actions实现定时邮件提醒每日待办事项的功能。也使用它实现了 Elixir程序集成测试 的功能。
Pulumi — 可编程的 IaC 工具。我很喜欢这个工具,因为可以使用非常少的代码就实现了自动创建AWS EC2服务器、配置域名、创建Serverless API、创建VPC安全组、申请HTTPS证书等复杂的手工工作,这么复杂繁琐的工作代码只需 几十行 就能搞定。由于完全的代码化,可以很方便的修改出一套新的基础设施架构,甚至可以对基础设施进行代码测试。
Terraform — 基于Yaml的 IaC 工具,由于其使用Yaml这种配置文件去做IaC,对个人项目来说有点复杂了,比如我在 双均线交易策略任务模块基础设施 中就是用Terraform实现定时任务所需的基础设施资源。相比之下我还是更喜欢Pulumi这种编程的方式去实现IaC,所以我用Pulumi代替了它。
Serverless — Serverless框架,可以很方便的定义Serverless API,我在 Invest Alchemy 中的API就是用此框架实现的。不过Pulumi也可以很简单实现它的功能,所以我用Pulumi代替了它。
Docker — 容器工具。一般个人项目涉及的服务少,服务器配置也低,所以我在个人项目中使用Docker比较少。比如在 双均线交易策略任务模块服务器环境 就是用Docker定义的。
Ansible — IT自动化平台。一般应用场景是利用Pulumi创建了基础设施中的服务器,需要对服务器做一些配置,比如安装软件及对软件做一些配置,服务器少的话人工登陆服务器去做配置就可以,但是如果服务器很多或经常需要需要创建一套新的服务器环境,那人工配置还是很麻烦的。我在 Invest Assistant 项目中利用Ansible完成了:
自动登陆服务器安装Elixir应用所需要的依赖包;
对应用程序依赖的环境变量做配置;
使用Let’s Encrypt设置HTTPS证书;
配置Nginx;
对系统进行性能调优,使其能满足应用高并发的系统配置需要。
监控分析
Logz — 在线 ELK 服务。如果自己搭建ELK的成本会非常高,因其对服务器的配置要求高。Logz提供的免费版足够个人项目做日志分析。我一般对流量站项目会做日志分析,比如查看页面的404/429占比、爬虫请求占比及爬虫请求量趋势等。
Google Analytics -网站流量监控分析服务。一般网站都会使用GA进行流量监控。我一般用GA + Search Console 综合分析网站 SEO 的效果。
Sentry -应用运行日志收集服务。我主要在App开发中会使用此服务做App的崩溃日志收集。
工具
好工匠需要好工具。
编辑器
VSCode — 早年尝试了不少编辑器,现在写代码与写文章只用VSCode,丰富的插件生态能够满足我的大部分需求,甚至我还修改发布过一个 TODO++ 的插件。
浏览器
Chrome — 我一般用Chrome自带的控制台调试Web网页。也会安装很多插件提高我的开发效率,比如这篇 我常用的浏览器插件 分享我常用的Chrome插件。
CLI
curl — curl很适合调试HTTP应用。使用curl可以完整看到HTTP交互的很多细节包括TLS握手过程、HTTP请求与响应的详情等。
lsof — 我一般用lsof查看端口占用的进程、进程打开文件及进程的网络链接。
netstat — 我一般用netstat查看当前端口占用、当前网络链接及TCP协议的某个端口监听进程。
具体详见我的Wiki笔记 网络调试。
设计
Sketch — 我曾用它做过App的原型设计,当然也可以用它设计文章配图。
Photoshop — 主要用它来修个图。
Figma — 目前很火的在线协作UI设计平台。可以用它来替代Sketch。
iconfont — 矢量图标库。我用它来代替 Font Awesome。
常用绘图工具 — 这篇 我的绘图工具箱 分享了我常用的绘图工具。
SaaS服务
Time is money. 用钱买 SaaS 服务是提高个人效率最直接的方式。
开发相关
GitHub — 源码托管平台。我一般用GitHub私有库存储个人重要的数据,公开库托管个人开源项目。当然GitHub也是一个社交平台,我会Follow一些我关注技术的专家,从他们的Timeline/Feeds中找到一些我没了解过的技术。
Namecheap — 便宜的域名供应商,我的所有域名都是在这上面购买的。
Airtable — 类Google Sheets的Low Code平台。可以使用表格存储数据,快速完成应用原型开发。
Google Data Studio — 简单上手的数据可视化平台。我用Google Sheets + Google Data Studio完成了我的 社交平台数据监控Dashboard。
邮箱服务
TinyLetter — 免费的邮件订阅平台。我用它实现博客的 邮件订阅 功能。
知识管理
Notion — Notion很适合团队协作。我用它管理一些公开的笔记或者家庭的笔记。我还建了一个 Notion好站列表 的Repo来收集不错的Notion公开笔记。
Logseq — Logseq是一款开源的笔记工具(甚至可以理解为一个基于数据库的应用)。我在 我的人生管理系统 分享如何基于Logseq去制定一套从个人愿景到具体阶段任务的人生管理系统。
OneNote — 我最早使用的一款笔记工具。我还专门写过一篇 用OneNote管理你的知识 的文章分享我的OneNote使用经验。但是OneNote是商业工具,同时不支持很多我想要的特性,最终我用Logseq替代了它。
与人协作
Zoom — 我一般用Zoom来与人协作,Zoom的视频会议稳定性不错。如果国内没法访问的话,可以用腾讯会议来代替。
安全认证
1Password — 密码管理工具。早期我也是几个密码走天下,但是这种风险很高,一旦主要密码被泄露(比如网站被脱库导致你的密码被黑客知道)就完蛋了。用密码管理工具的好处在于每个网站用一个安全的随机密码,登陆的时候用密码管理工具自动填充密码登陆。
Microsoft Authenticator — 微软的两步验证App。我用来代替Google Authenticator(这个很久不更新了,担心某天突然找不到这个App)。
广告平台
Google AdSense — CPC计价的广告平台。本博客对接的广告平台就是Google AdSense。中文的广告收入特别低,可以忽略不计。如果是英文内容站,内容质量还不错的话,那接入AdSense是个不错的选择。互联网在线广告是互联网流量变现的主要方式,也是个人项目盈利的一个很重要的途径。如果对广告不了解,可以看我这篇 什么是互联网广告。
Adsterra — CPM计价的广告平台。和AdSense的差异在于计价模型的不同。如果你的网站流量特别大,但是页面价值很低(通俗的话就是垃圾站)的话,可以考虑对接这个平台。
Affiliate平台
个人项目盈利的几个重要途径有:
互联网在线广告,适合内容综合的流量大站;
Affiliate,适合内容垂直的中小站。这种方式的关键是有精准的流量,转化率比较高;
一次性销售费,适合工具类产品;
会员订阅费,适合内容类产品。
Affiliate在国内很常见,比如国内的淘宝客。简单的理解就是自己有流量渠道,可以帮助广告主销售产品或服务,中间抽取佣金。我们个人项目有流量的话就可以考虑通过这种方式变现,如何找广告主接他们的推广任务?那就是通过Affiliate平台。国际上知名的Affiliate平台有以下两个:
ShareASale
CJ Affiliate
收款平台
赚了钱如何低成本收款也是一个麻烦的问题,尤其是你赚了不少美刀。这里提到的都是国外的收款平台。
PayPal — 国外收款很方便的方式。缺点就是收款和转账手续费都很贵,转美元至国内结汇还得支付4.4% + 35美刀的手续费。PayPal的坑非常多,如果真决定要此渠道收款,建议先了解下具体的操作流程和收费规则。
Payoneer — 这个和PayPal的区别在于它的手续费很低,不过我接入的一些广告平台并不支持此收款方式,以至于我并不了解它有什么大坑。
Bitcoin — 比特币收款是很上头的,因为价格波动剧烈,你永远不知道收到的钱是多了还少了,而且比特币转回国内也是个麻烦的操作。
Stripe — 这个收款平台适合你直接从用户手中收款,它支持信用卡等多种支付方式,比如你有个独立的产品卖许可证,用户通过信用卡支付给你钱这种场景。
其他
Storm Proxies — 老牌的IP代理服务。特点就是老牌、便宜,稳定性还可以,延时比较高,无法满足对实时要求高的爬取需求。
Surge — Mac上的高级网络调试工具。
Quantumult — iOS上的高级网络调试工具。
Google Voice — 一般用它来接受国外网站的注册验证码。
YouTube Premium — 学习娱乐两不误的视频网站。
Netflix — 一般在电视上用它刷剧。
如何调研产品的技术栈
学习别人的技术栈,查缺补漏。
Wappalyzer — 浏览器插件,可以看到某个网站使用的技术栈。虽然结果不全面和准确,但是足以了解到很多新技术栈。
stackshare — 这网站上可以看到不少知名网站使用的技术栈。
官方博客 — 一般官方技术博客会分享其应用所用的技术栈,通过这种途径可以了解到他们为何会选择这种技术栈及他们技术栈演进的过程,这方法虽然费时但是效果非常好。
合规问题
遵守规则才能走的更长远。
公司规定
一般公司都会有关于员工在业余时间的产出归属问题的规定。在做个人项目时需提前了解下公司的规定或态度,这样才不会在后期出现难以预料的问题。
知识产权
很多开源软件有License的选择问题。也有不少工具对非盈利项目提供免费License, 但对商业项目需购买收费License。这些问题都需要在做个人项目时时刻注意。
法律法规
在做个人项目时,可能会不小心游走在法律的边缘,比如爬虫很容易引发法律的问题。某些产品做出海的业务也会遇到跨国法律的问题。提前了解下相关法律是规避这类问题的好办法。
进阶阅读
个人项目技术栈 — 这个Repo会收集一些适合个人开发者的技术栈或者研究个人项目盈利方法论。
一人公司方法论
更新时间: 30个月前 版本: 4a4a6beef
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
编程语言是如何实现并发的之并发模型篇
编程语言是如何实现并发的之操作系统篇
编程语言是如何实现泛型的
基于Serverless实现静态博客访问统计功能
Serverless应用开发小记
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
失败驱动开发
23 May 2021 15:54 | 3499 字 | 2944 次阅读
个人成长
FDD • Clojure • Functional Programming
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
多年以后,面对这篇文章,我会想起那两天失败的令人崩溃的开发过程。当时,只是一个简单的编码需求,我信心满满的计划一下午搞定,但是最终的过程却是令人如此沮丧,让我不得不怀疑我还适不适合继续当程序员。
思绪飘到那天的场景,我在开发过程中遇到一个很简单的需求:将JSON格式的文件转换成JavaScript的常量文件。如下图:
我的想法是先把JSON转成一棵抽象语法树(AST),然后遍历这棵树,在特定的节点打印出所需的字符就可以了。JSON转AST直接用Clojure的神器instaparse库。我对Clojure不熟悉,刚好可以通过这个过程提升下,也能试试这个神器到底神不神。通过这种特殊需求能一举多得,让无聊的开发过程变得有期待。
第一步是将JSON转AST。对于instaparse库来说这是个很简单的任务,网上随便搜索下就找到了解析JSON的代码。耗时不过几分钟。
第二步是需要遍历这棵树。遍历树是我在大学算法课程上就学过的,虽然年代久远算法的细节都已经忘记,但是我还记得有深度遍历和广度遍历两种方式。我的这个需求特殊之处在于需要在遍历的时候打印相关的字符,比如需要在遍历某个节点开始和结束的时候都得打印[]或{} 。Clojure应该有具体的库做这个事,简单搜索下很快就找到了walk和tree-seq这两个函数。这两个函数看起来比较复杂,找了一些例子大概了解到: walk函数可以在遍历是提供入和出两个钩子来执行对集合元素的转换,而tree-seq会以深度遍历树的方式输出一个节点序列。理解后就开始尝试,花了半天后发现事情比我想象中的复杂,这两个函数看起来强大,但是无法在遍历节点时保存状态,而我却需要这个状态来记录我遍历的路径。看起来需要自己写个遍历算法来实现了,这时候半天已经过去了,但我目前的进度只解决了一半的问题。
自己写遍历树的算法是一件不难的事情,我用Java也实现过,现在用Clojure实现看起来也不难。但是Clojure和Java的差异很大:它是函数式的,数据类型都不可变,很多操作都是通过递归来完成。用递归来实现深度遍历也不是难事,但是当你用不熟悉的语言去实现问题可能就会变得不可控。
在尝试了一天多并写了三个失败的版本后我陷入了绝望的状态,因为一个非常简单的问题我却搞不定。在第二个版本的时候我以为我解决了这个问题,最终把实际的数据输入却发现结果不符合预期。因为我用了简单的测试数据,实际的数据比测试数据全面,我写的版本只是解决了测试数据的问题。在第三个版本的时候因为考虑的情况更多写的也更复杂了,导致程序始终跑不起来。因为我不熟悉Clojure的语法,始终难以写出满足条件的递归代码。
由于长时间在这个问题上耗着又没有任何思路,我在周末连续搞了十几小时后眼睛和腰终于受不了了。第二天整个人身心俱疲,在床上躺了半天后琢磨如何寻求帮助。脑海中第一个念头就是在Clojure的社区里直接提问。为了能让大家有意愿回答我的问题,我首先把自己的问题梳理了下,画了一个简单的草图:
然后在StackOverflow提了这个问题,并在Clojure的Discord群组、Telegram国内社群和微信群里发了这个问题。大概不到半小时,微信群里有两个人发了自己的代码。这两种代码体现了不同的解决思路,并且附带优雅的实现:
第一种方案直接通过递归将AST语法树转换成了目标Map的数据结构,然后使用Json库打印成Json格式。第二种方案没有使用AST语法树,直接通过Json库拿到Json数据结构然后递归遍历输出最终目标数据结构。
在群里与这两个人沟通的过程中,我发觉我在不知不觉中犯了几个错误:
不熟悉Clojure代码,导致没法使用最佳的函数和思路去解决问题;
通过Json库去输出最终数据结构,而我却是采用打印的方式将问题复杂化;
没必要通过抽象语法树去解决,通过Json库递归遍历Json是更简单的方案;
没使用更好的工具。我一开始用命令行自带的Repl,后来觉得编辑长函数不方便,所以在网上找了一个在线Repl。不过后来看到群友提供的在线livebook, 这种能更方便的开发并记录这类代码。
回顾这个问题的解决过程,我总结此次开发失败的原因有以下:
理解需求错误。我在遇到这个问题后并未做深入的分析思考,导致一开始就冲着问题的表象去解决。想着用打印的方式去解决问题,实际上可以用库来输出目标格式。
不熟悉相关技术。我对Clojure的熟悉程度还不足以解决这类并不简单的问题。
解决问题不全面。问题总有很多解,拿着锤子很容易看啥都是钉子。我从一开始就想通过AST去解决这个问题,导致思维局限到一条线上了。
害怕失败。因为一开始觉得问题很简单,害怕自己没法在很短的时间解决,心态处于失衡的状态。后期耗着的时间越长,思考能力越不在状态,反而越来越迷糊。
失败驱动开发
不了解程序员的人眼中的程序员可能是这样的:
但开发程序或维护程序,失败是很常见的:
编译失败;
运行失败;
网络失败;
内存失败;
并发失败;
I/O失败;
认证失败;
权限失败;
依赖失败;
资源失败;
上线失败;
升级失败;
环境设置失败;
理解需求失败;
项目管理失败;
架构设计失败;
程序员的日常就是要在无数失败中找寻让程序正常运行的那一种组合,成功运行更像是运气与实力的双重作用,这也就有了失败驱动开发(Failure Driven Development)。
失败既然是不可避免的,要做好一个程序员,与失败平和相处是必须要解决的问题,不然情绪会长期处于失衡状态。
如何以失败驱动开发?我会从以下清单出发找寻处理失败的方法:
是否全面理解问题? 很多时候不是问题复杂,而是我错误的理解了问题,在错误的路上越走越远。每当失败时我会重新全面的思考问题,看是否能发现新的解决问题的思路。
是否涉及知识盲区? 盲区是你不知道自己不知道。用有限的知识去解决未知的问题很容易陷入盲区而不自知。我的方法是如果一个失败的原因我没法在几天内解决,那很可能就是遇到知识盲区了。要跳脱盲区必须全面的搜索关联的知识,通过知识的交叉理解或寻找更了解这个领域的人帮忙是有效的解决方法。
对技术的掌握是否满足要求? 用不熟悉的技术去解决不懂的问题很容易失败。如果对技术不熟悉并且难以解决问题的话,我会从短期和长期两个方面出发制定不同的方案。短期可能会寻求外部帮助让更了解的人来帮我解决,长期我会投入更多时间提升这方面的技术。
所用技术或工具是否合适? 用不合适的技术和工具去解决问题也很容易导致失败,并且这种失败是难以察觉的。有时候不合适的技术或工具并不会让问题无法得到解决,而是会浪费你大量的时间去解决技术或工具本身的问题。要解决这类失败需要扩大知识广度,在搜索资料时不局限某一种技术,如果你对多种技术有一定的理解,就很容易发觉技术之间的差异。用合适的技术或工具能达到事半功倍的效果。
是否存在解决方案? 很多问题早已经被前人解决。所以当遇到感觉复杂的问题,我会先搜索一番已经存在的解决方法,对问题现存的解决方法有个大概的认知,然后修改这些解决方法让其能更好的解决我的问题。
是否需要记录问题? 各类很难搞的问题是提高能力的好机会,学习现存的解决方法能消灭知识盲区。所以不断的记录总结这种问题是提高我能力的好方法。如果一个人一辈子遇不到难题,他也只能停留在现有的能力圈无法破圈。
是否需要寻求帮助? 花了很多时间问题却解决不了是很令人沮丧,有些问题还很紧迫。在尝试一定时间还毫无头绪时我就会想办法找人帮忙。让人愿意帮忙也需要一些技巧,如果你提出一个很大的问题,没人会愿意免费帮忙。所以我会把问题相关的上下文都写下来或画下来,然后将我错误的解决方法放上去,标记清楚失败的点在哪里,然后把问题发给我觉得有这方面技术的朋友、同事及相关的社区。如果问题比较复杂,我会提出付费咨询的请求。在别人帮忙解决后,及时表达感谢之情,如有必要也可以发个红包。当你通过这种方法认识不同领域的人,逐渐地你解决问题的效率也会得到提高。一些人会担心,将自己的愚蠢公开暴露出来,尤其是一些低级错误出现的时候,是一件很掉面子的事情。其实一开始我也担心,但是在网上你可以有很多虚拟身份,能缓解这种不适。更重要的是,暴露自己的愚蠢能有效的解决自己的知识盲区,你觉得很复杂的问题在有经验的人看来是很简单的事情。这其实是一种极其有效的学习成长方式,在这个过程中我不仅可以解决我的难题,还能学习有经验的人在这领域里的方法论和效率工具。
身体状态是否合适? 长时间耗在一个问题上,身体和大脑都会疲惫。当心态失衡时,解决问题的能力也会直线下降。我经常会陷入一种急迫解决问题的困境,直到身体完全扛不住才放弃。这其实是一种低效的方式,情绪会在这个过程中逐渐压制理智,让人很难全面的思考问题。与自己平和相处,接纳自己的不足,休息好重新出发才能走的更长远。所以当遇到自己很难解决的问题时,试着先确保身体状态是正常的,如果身体很疲惫,先休息而不是直接攻克难题。
每一次失败都是一次提升自己的机会。正是对失败过程的不断迭代解决,多年以后,让我成为一个更好的开发者。
更新时间: 35个月前 版本: 8171f612c
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
国际化与本地化
27 June 2021 41:4 | 9036 字 | 7179 次阅读
什么是X
i18n • l10n
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
国际化(i18N)
国际化需解决的问题
国际化相关标准
文本编码
locale与language tag
语言与国家代码
gettext
国际化流程
本地化(L10N)
本地化流程
制定本地化策略
地域与语言
新增地域/语言/服务
增量本地化
翻译的管理
本地化的实施方式
本地化多语言的实现方式
本地化的挑战
是否需要考虑SEO
产品设计的本地化
微服务下的本地化
本地化的技术或业务标准制定
开发环境与业务流程
静态文本的处理
是否存储语言与地区设置
后端服务的本地化
第三方服务与资源的本地化
发布流程
微前端架构下的本地化
本地化的测试
本地化平台
本文已翻译成以下语言版本:
英文:Internationalization(i18n) and Localization(L10n)
西班牙语:Internacionalización (i18n) y localización (L10n)
一个成功的产品要走向全球需要经历很多环节,从软件开发的视角主要有国际化和本地化两个流程:
语言环境是在某个国家或地域内使用特定的语言或语言变体,其决定了日期、时间、数字和货币的格式和解析方式,以及各种测量单位和时区、语言、国家与地区的翻译名称。国际化使一个软件能够处理多个语言环境,本地化使一个软件支持一个特定的地区的语言环境。这意味着全球化的流程是先使软件具备国际化的能力,之后做本地化实施使其能支持特定地区特定的语言环境。
基于他们的英文单字长度过长,常被分别简称成i18n(18意味着在“internationalization”这个单字中,i和n之间有18个字母)及L10n。使用大写的L以利区分i18n中的i和易于分辨小写l与1。(Wikipedia)
国际化(i18N)
国际化需解决的问题
能够以用户本地的语言显示文本;
能够以用户本地的语言输入文本;
能够处理以特定编码的用户本地语言的文本。
国际化相关标准
我们知道国际化是为了解决与用户本地语言相关的文本显示与输入的问题,这个问题又与用户国家和语言相关,比如同样的英语在美国和英国就不同。在国际化标准还未出现之前,曾经有多种表示国家与语言的方法,这个 Making Sense of Language Tags 的Slide就分享了这段有趣的国际化标准问世的历史。直到IETF BCP(Best Current Practice) 47的出现,才统一规定了国际化中语言标识(Language Tag)的定义及匹配标准。
而由于很多软件与系统早于此标准出现,就会出现一些与此标准不统一的问题。一个突出的问题就是语言标识定义中的连接线的选择,在Linux系列的操作系统中用locale来定义语言环境,如en_US表示美国英语,而在BCP 47中用en-US表示美国英语。前者选择了用_而后者采用了-来连接语言和国家。这种混乱有时候会带来很多意想不到的困惑,有时候你使用的某个库支持en_US,有的库却支持en-US,这不得不让国际化实现的过程中多了一些兼容性处理的工作,甚至因为语言不统一,出现很多沟通上的问题。
国际化相关的标准如下:
IETF RFC 6365:统一定义了和国际化相关的术语。
IETF BCP47
RFC 4647:制定了如何通过过滤(Filtering)和查找(Lookup)的方式匹配语言标识(Language Tag)。
RFC 5646:定义了语言标识(Language Tag)的组成,如使用en-US标识美国英语。
ISO
ISO 639:语言编码(Language codes)标准。
ISO 3166:国家编码(Country codes)标准。
ISO 15924:脚本(Script codes)标准。
一个完整的语言标识(Language Tag)组成如下:
更详细的介绍见我这个 i18N in Java 的Slide。
文本编码
不同编码有着可以表示不同字符集合的区别,比如我们无法用ASCII编码来表示汉字。Unicode字符集可以用从0到10FFFF (十六进制)范围的码点来显示几乎所有人类已知的字符。它的存储至少需要21位。文本编码系统UTF-8将Unicode码点适配到一个合理的8位数据流,并兼容ASCII数据处理系统。UTF表示Unicode转换格式(Unicode Transformation Format)。
自2009年以来,UTF-8一直是万维网的最主要的编码形式。截止到2019年11月, 在所有网页中,UTF-8编码应用率高达94.3%(其中一些仅是ASCII编码,因为它是UTF-8的子集),而在排名最高的1000个网页中占96%。所以在国际化中推荐采用UTF-8编码。
这篇 IT产品的国际化,绝不是“支持英文”就足够 文章提到一些GBK编码的文本中有许多“看起来一样”的文字,其实有细微差别。但是,为了节省Unicode中的空间,给它们指定了同样的Code Point。
如何区分这些同样码位(用不同字形显示一个字符,即同一字位)的同位异字?这就需要locale的帮助了。
计算汉字数量时,通常是按照字形来计算的,即将一个代表相同语音语义的字的简化,繁体,异体,新字形,旧字形等等分别进行计算。这种计算方式实为是在计算变体。所以,长期以来错误地把大型字典里收入的字形数看作是汉字系统的规模。(Wikipedia)
locale与language tag
locale是软件在运行时的语言环境,它包括语言(Language),地域(Territory)和字符集(Codeset)。locale使用language tag标识语言国家,比如在GNU Linux中的定义格式为:语言[_地域[.字符集]],如美国英文是en_US.UTF8。在Linux中locale包括以下几个部分:
LC_COLLATE:控制字符排序。
LC_CTYPE:控制字符处理函数在处理大、小写或判断是否是字符。
LC_MESSAGES:提示信息的格式。
LC_MONETARY:货币的格式。
LC_NUMERIC:数字的格式。
LC_TIME:时间的格式。
如果你的locale是en_US.UTF8,那么必须将其修改为zh_CN.UTF8才能正确显示中文。在macOS操作系统的 /usr/share/locale 目录中存放着全部支持的locale:
而在BCP 47中,language tag的定义为langtag = language[”-” script]["-" region]("-" variant)("-" extension)["-" privateuse]。
语言与国家代码
同一种语言在不同国家地区可能有一些细微的差异,比如美国英语和英国英语就有一些差异。同一个国家可能也有多种语言,比如中国有简体与繁体语言。在上述locale的介绍我们看到了使用语言_地域或语言-地域的方式来确切的表达一个国家的语言。
对于国家和语言ISO制定了相应的标准代码:ISO 3166-1 与 ISO 639-1。
浏览器使用语言代码来在Accept-LanguageHTTP头里发送浏览器接受的语言名。比如:it, de-at, es, pt-br 。
gettext
GNU gettext是GNU国际化与本地化(i18n)函数库,它常被用于编写多语言(multilingualization,缩写为M17N)程序。许多编程语言如C、C++、Python、PHP、Rust、Elixir等都在语言内部支持了gettext的使用。
以下是Java调用gettext完成国际化的流程:
xgettext扫描源代码抽取出tr()、trc()和trn()这些i18n函数的输入字符串并创建一个包含所有源语言字符串的pot文件。翻译者需要工作的对象是.po文件,它是由msginit程序从.pot模板文件生成的。
msgmerge将字符串合并到一个包含单个语言环境翻译的po文件中。
msgfmt用于生成继承JavaResourceBundle类的Java类文件。
如下图是PHP使用gettext实现国际化的流程图:
Elixir使用gettext实现i18n的目录结构:
priv/gettext
└─ en_US
| └─ LC_MESSAGES
| ├─ default.po
| └─ errors.po
└─ it
└─ LC_MESSAGES
├─ default.po
└─ errors.po
国际化流程
gettext的使用流程就是一个典型的使应用支持i18n国际化的过程:
配置i18n框架。i18n框架通过系统或者浏览器(如果是Web应用)的语言标识自动获取相关的语言文件。如gettext使用的是.mo后缀的文件,而Javascript一般是.json文件,Java是.properties文件。
抽取硬编码的源语言文本。在硬编码的地方调用i18n函数。对于这部分可以人工抽取,也可以通过程序或者插件(如Javascript的i18next国际化框架有i18next-scanner)自动抽取。
最后实施本地化。翻译(可通过人工或机器翻译,也有相关的翻译平台可以集成)这些抽取出来的要支持的国家语言文件。
本地化(L10N)
本地化流程
如上图是一个典型的本地化流程图。其中参与方有:
开发团队(Dev Team):开发人员使系统具备国际化的能力,并将机器翻译版的多语言版本部署到集成环境供测试人员测试,能搭建自动化的翻译集成流水线。
市场团队(Market Team):确认产品的市场和支持的语言,整理产品涉及到的术语表,并购买专业翻译服务,确定最终多语言翻译的版本。如果是大公司的话,可能有专门的全球化团队完成此工作。
翻译管理平台(TMS):完成翻译语言的管理,一般都有特定的API接口或SDK开发工具包,可以集成到CI/CD环境,能自动化源语言和翻译语言文件的上传和下载。并具备管理界面,供翻译人员修改和确定翻译的最终版本。能提供多个机器翻译服务,也能提供人工翻译的购买或以开源协作的方式完成人工翻译。
制定本地化策略
地域与语言
这块首先要考虑这些基本的前置问题:
地域的业务含义
用户的默认地域
地域的默认语言
不同地域是否使用同一套系统
是否支持用户切换地域
用户能否属于多个地域
地域与国家是否一对一关系
地域与语言的映射关系
地域与语言有无联动关系(用户是否能看到所有地域支持的所有语言)
语言切换是否需要保存到用户个人信息中
是否需要通过用户环境语言标识(操作系统或浏览器)设置用户默认语言
服务是否多地域部署,多地域数据是否隔离
新增地域/语言/服务
系统是否可以支持新增地域及新增地域的流程
系统是否可以支持新增语言及新增语言的流程
系统新增子服务时本地化的流程
增量本地化
当实施本地化时新页面或组件出现时的本地化流程
翻译的管理
是否需要翻译管理平台(TMS)
翻译管理平台的选型
翻译管理平台的集成
是否需要订购专业翻译服务
开发与翻译团队的协作流程
本地化的实施方式
系统各服务是否由各自开发团队做本地化
是否有专门的本地化团队做本地化
本地化团队与各服务开发团队的协作模式
是否通过代码Open PR的方式做本地化
各服务开发团队如何做增量本地化
各团队关于本地化的知识同步
本地化技术标准的制定与组织内部推广
针对日期、时间、时区、数字和货币的特定语言格式使用行业标准库(例如Unicode Common Locale Data Repository CLDR)
locale标识采用语言_地域或语言-地域格式,如en-US代表美国英语语言。
本地化多语言的实现方式
通过子域名(gTLDS)或国家顶级域名(如ccTLDs)区分多语言。如:https://en.wikipedia.org/
通过URL路径区分多语言。如:https://localizejs.com/de/
通过URL查询参数区分多语言(对SEO不友好)。如:https://locize.com/?lng=de
通过用户语言设置区分多语言。如:https://myaccount.google.com/language
通过浏览器本地存储区分多语言。如:https://www.instagram.com/
本地化的挑战
本地化的挑战主要有不同地域的语言、文化、书写习惯及法律方面的差异带来的问题,具体有以下类别:
文本编码:对于大多数西欧语言的文本,ASCII字符编码就足够了。但是,使用非拉丁字母的语言(例如俄语、中文、印地语和韩语)需要更大的字符编码,例如Unicode编码。
单复数:不同语言有着不同的单复数形式。复数是用来表示一个“不是一”的数。单复数的变化型态在每个语言里面都不一样,最普遍的复数型态用来表示二或更大的数字。在某些语言中,也有用来表示分数、零、负数或者二。
图片翻译:有文字的图片需要被翻译。
动态数据(来自API的数据):后端传给前端的被显示在界面上的数据都需要本地化。但是如何区分这些数据的来源是个难题,比如虽然数据是来源自后端的,但可能来自数据库,可能来自文件,可能来自内部其他服务,也可能来自第三方依赖的服务。
图标:一些在某个地区识别度很高的图标在其他地区用户看起来可能是完全不认识的或者是其他东西。
姓名/地址:姓和名的先后次序,地址书写的先后次序。比如中文都是先姓后名。
性别:有些语言比如法语很强调性别。
电话:不同国家的电话格式也不相同。
声音:不适当的声音或提示可能会引起人的反感,有些国家对声音性别很敏感。
颜色:颜色和色调与地域或市场有关,比如红色在美股标示下跌,在A股标示上涨。
计量单位
货币:货币格式设置必须考虑货币符号、货币符号位置和负号的位置。大多数货币使用与区域性或区域设置中的数字相同的小数点分隔符和千位分隔符。但是,在一些地方并不是这样,比如在瑞士,瑞士法郎的小数点分隔符是句点。
日期和时间:日期/时间的国际化,不仅涉及到地理位置(比如星期、月份等日历本地化表示),还涉及到时区(TimeZone,针对UTC/GMT的偏移量)。时区不仅是地理位置规定,更是政治规定,比如中国从地理位置上跨5个时区,但只使用一个统一时区。另许多国家都有“夏令时”的规定,柏林时间和北京时间的差距是会变化的。有时候是7小时(冬令时),有时候是6小时(夏令时)。
数字:不同国家和地区数字表示方式也存在着区别。影响数字表现的因素包括数字字符的表示、数字符号的表示、数字的类型等。
重量/长度/物理单位:因为单位的不同,同一套数据多地域版本需要做转换。
业务相关的计量单位:比如不同国家产品的计费规则不同。此需要业务人员支持找出相应位置并给予换算规则。
句子长度:德语通常比英语长,阿拉伯语需要更多的垂直空间。
书写方向:许多语言是从左到右,但在希伯来语和阿拉伯语中是从右到左,在某些亚洲语言中是垂直的。
标点符号:例如英语中的引号("")、德语中的低引号(,,")和法语中的引号(«»)。
换行/分词:亚洲CJK(Chinese、Japanese、Korean)字元集语言的规则与西方语言的规则完全不同。例如,与大多数西方书面语言不同,中文、日语、朝鲜语和泰语不一定使用空格将一个字同下一个字区分开。泰语甚至不使用标点符号。
大小写转换:英文中有大小写的转换,而中文没有大小写的区别。
法律相关:例如使用欧盟公民个人数据的GDPR。
政治相关:比如本地化中涉及国旗和地图的显示,处理不好很容易造成大的事故。
排序方法:比如英文是按字母顺序排序的,而中文可以用拼音排序。
是否需要考虑SEO
如果是toC的网站做本地化,需要考虑一些和搜索引擎优化(SEO)的事情,如这篇 How to approach an international strategy 提到的一些关键点:
如果您以多种语言提供您的网站,请在每个页面上使用单一语言进行内容和导航,并避免并排翻译;
将每种语言的内容保留在单独的URL上,并在URL中标记语言。例如,URLwww.mysite.com/de/会告诉用户页面是德语的;
通过hreflang元标记向Google显示您要定位的语言。如;
不要只翻译模版文本,还需要翻译模版内的内容;
不要完全使用自动翻译,这会影响用户体验;
不要使用cookies或脚本技术来切换语言,Google爬虫无法正常索引这些内容。
产品设计的本地化
同一内容在不同地域使用更贴合本地内容的设计会带来更好的效果,如 产品设计的国际化与本地化 这篇文章中提到Spotify的歌单封面在不同国家的差异呈现形式:
微服务下的本地化
从架构的角度看单体应用的本地化流程比较简单。但现在很多应用都是微服务架构,多个团队协作开发的模式。如果是各自团队负责各自服务的本地化,必须有统一的本地化委员会制定本地化技术标准:
语言标识的确定;
语言切换在前后端的方案设计;
翻译自动化流程的设计等。
或者有专门的本地化团队实施本地化,前面这些问题将由这个团队负责解决。笔者参与的项目就属于后者,我们的团队完成整个大系统近十几套微服务子系统的本地化,这十几套系统又由几个大组多个团队负责,这类跨功能需求(CFR)在多个团队中的协作流程是个复杂的工作。
本地化的技术或业务标准制定
在实施本地化之前,确定相关技术或业务标准是重要的事情,一些技术或业务标准有:
前后端不同技术栈的国际化实现标准。由于微服务中技术栈可能有多种,每种技术栈都有其各自的国际化实现方式,制定不同技术栈的实现标准有助于在不同服务间使用同样的实现方式;
locale标识的确定。
前端或后端静态文本抽取中,可以将和语言相关的文本存放至语言标识命名的文件中,如en.json存放英语的静态文本,而en_US.json存放和美国英语相关的文本(如计量单位、日期、数字和货币等);
在远程服务调用(前端调用后端或后端调用其他内部或外部服务)中统一采用语言-地域格式,如en-US代表获取美国地区英语语言的本地化版本。
日期、时间、时区、数字和货币的特定语言格式使用行业标准库。比如使用实现 CLDR 标准的库;
动态数据类型的识别。比如识别出哪些数据是来自内部系统(数据库或文件);哪些来自外部系统;这些动态数据是否具备国际化能力;如何分阶段本地化这些数据;
文档的本地化。后台系统生成的电子文档(PDF)或电子邮件的本地化。如果这些文档是发给客户的,还需要考虑是否生成客户语言偏好的文档;
支持的地区与语言列表。比如出现不支持的国家或语言时进入错误页还是显示默认的地区或语言本地化版本;
默认地区与默认语言;
地区与语言是否具备绑定关系;
语言切换是否需保存至用户个人信息中。
开发环境与业务流程
实际上我们团队在做本地化最耗费时间的就是本地环境的启动。因为涉及的服务众多,不同服务启动的方式又有着细微的差异,甚至指导文档也是错误的,需要不断的踩坑才能完成环境的搭建。最终我们的处理方式是联系各开发团队,每次在做某个服务的本地化前期,会找开发团队帮助我们设置本地的环境。
另外一个难点在于我们对业务的不了解。由于每个服务都有大量的组件和页面,包括后端服务不同来源的动态数据,光靠我们自己摸索很难搞清楚。最终我们在做这个服务的本地化前期,会找开发团队的业务分析师帮助我们介绍这个服务涉及的业务流程。
静态文本的处理
梳理前后端的静态文本,识别哪些系统有国际化能力(初始语言版本已经抽取locale文件并设置好国际化库);
找出日期、货币、数字格式出现的地方,并在这些地方调用本地化技术标准确定的行业标准库;
增量静态文本翻译流程的确定,当系统已经被本地化之后有新的文本添加时需要使用本地化的流程对其进行处理;
翻译平台的自动化集成,开发团队使用脚本或CI/CD流水线自动上传和下载原语言和翻译语言的文件。
是否存储语言与地区设置
一些国际化站点的语言或地区切换设计成超链接,用户可以通过链接访问不同语言地区版本的站点,这类站点并不需要存储语言或地区的配置。
具备用户个人信息配置的站点一般会提供在个人信息设置中设置偏好语言和地区,这样用户在切换设备的时候可以同步上次设置的语言或地区。
如果你的站点用户切换设备并不频繁,简单的处理可以将这些配置存入浏览器存储中。当用户切换设备后,自动恢复默认设置。这样的设计好处在于简单,后期要过度到其他方案也会容易一些。具体选择什么设计,需要结合具体业务来选择。
后端服务的本地化
后端服务的本地化涉及以下四部分:
静态文本。这类可以通过走读代码的方式去查找相关字符串;
数据库、缓存或文件。通过走读数据库初始化脚本可以查找到不满足本地化需求的初始数据,但对于动态存储的数据,还需要设计满足多语言存储的表。对于一些资源文件有必要翻译的也需要提供多语言版本并改造使用文件的代码;
远程调用其他内部服务(RPC)。内部服务调用的locale标识属于本地化技术标准制定的。比如可使用locale = en-USHTTP头代表请求美国地区英语语言的页面。
生成文档(PDF或Email)。生成的文档包括模版静态文本与动态数据渲染的最终语言版本。尤其是这些文档和电子邮件需要发送给用户的时候,需要生成和用户语言相符的语言版本。
如果后端服务的技术栈不同,还需要本地化团队总结后端服务不同技术栈的国际化流程,并在组织内部同步给其他开发团队。
第三方服务与资源的本地化
在后台服务远程调用中存在调用外部服务的情况,如果调用外部服务需要先确认外部服务是否支持多语言版本,如果支持的话可以按照对接文档来集成。如果不支持需要与外部服务供应商联系确定支持计划。
发布流程
由于本地化的实施涉及十几个子服务的改造,可通过 Feature Toggles 控制本地化在不同环境的开启或关闭。本地化影响的测试(单元测试、集成测试与UI测试)也需要通过Feature Toggles来控制,这样可以最小影响原服务的测试套件。
一旦所有服务都完成了本地化实施,则可以打开所有服务的本地化Feature Toggles将最终版本上线。
关于本地化的Feature Toggles有两种设计可以选择:
集中式Feature Toggles。比如可以搭建一个集中的特性配置服务,所有本地化相关的服务通过请求此服务来获取配置开关状态。好处在于无需重新上线即可实时开关本地化的特性。坏处在于没法灵活控制每个服务的本地化特性的开关。
独立Feature Toggles。与集中式相反,每个服务设置自己的本地化Feature Toggles可以做到灵活解耦,坏处在于每次开关都需要重新发布上线单个服务。
微前端架构下的本地化
如上图是一个微前端架构的网站,整个网站的界面是由A/B/C/D/E五个服务的页面组成的。语言切换按钮在服务A上,当用户切换英文到中文,其他服务B/C/D/E需要将各自的界面切换成中文语言版本。
一种方法是在浏览器加载页面的时候将国际化(i18n)库的实例统一由服务A来初始化并挂载到浏览器窗口(window)对象上,服务B/C/D/E使用服务A初始化的国际化库实例对象。当语言切换时,统一由服务A的国际化实例对象切换所有服务的语言。
每个服务的locale语言文件加载可以统一由服务A来加载到浏览器中,这种做法的好处在于可以知道最后一个语言文件加载完毕的时机,这意味着整个页面所有服务的本地化都初始化完毕,用户可以正常切换语言了。
本地化的测试
本地化测试验证应用程序或网站内容是否符合特定国家或地区的语言、文化和地域要求。
如上图是本地化测试需重点关注的点,更多详见这篇 Localization testing: why and how to do it 文章。
本地化平台
本地化中很重要的一块是选择合适的翻译管理平台(TMS),一般这类平台都有如下功能点:
术语表(Glossary):专用品牌术语或领域术语表,可以帮助翻译人员更准确的翻译和产品或市场相关的专用语言;
翻译记忆库(Translation Memory):TM是一个数据库,用于存储之前翻译过的内容字符串。对相同或相似内容重复使用翻译。这样可保证翻译的一致性;
上下文编辑器(In-Context Editor):这种编辑器可以抓取网站页面,让翻译人员了解整个页面的上下文,有助于提高翻译质量;
机器翻译(Machine Translations):大多TMS平台都对接了一些机器翻译平台(如Google翻译),可以自动翻译目标语言,适合开发人员使用;
人工翻译(Human Translations):可在TMS平台订购专业人工翻译服务。但也有如Crowdin提供了开源项目本地化翻译协作的功能,任何人都可以参与到这个项目上免费翻译,得票高的翻译文本将优先使用。
主要的本地化平台:
Crowdin
Lokalise
localizejs
Phrase
一些对国际化以及本地化的基本流程的介绍就到此为止了。本地化是个复杂的工作,最大的难点在于对目标语言和文化的了解不足。不过当你读完这篇文章后,我希望能给你更多的自信去做本地化相关的工作。
更新时间: 30个月前 版本: 3f8466494
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
云端IDE
23 August 2021 16:19 | 3590 字 | 9900 次阅读
什么是X
AWS • Pulumi • VSCode • AWS Cloud9 • GitHub Codespaces • IDE
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
云端IDE
为什么是VSCode
良好的设计
跨平台
开源
基于AWS与Pulumi搭建个人云IDE
远程访问
关于费用
云IDE研发模式
参考文章
更新时间:2021/10/11
最近我将VSCode Server从AWS迁移到了Azure云平台,并且集成了OAuth2 。具体的实现见 Out-of-the-Box-CodeServer。新的实现方案可以做到:
1、关闭浏览器一定时间后自动关闭服务器以停止计费;
2、使用GitHub OAuth2登录;
3、使用自定义域名访问。
前不久GitHub官方Twitter发了一个如下的Tweet:
只要你在任何GitHub Repo页面上按下.键会自动跳转到github.dev的网站,此网站是一个网页版的VSCode并且会自动克隆下这个Repo的代码。在这个网页版的VSCode里你甚至可以安装一些特定的插件(无法安装需要外部依赖的插件),能更方便的阅读代码。因为这个网站是官方出品,这个VSCode已自动绑定了你的GitHub账户,开发者可以在里面阅读、编辑及提交代码,整个过程行云流水,甚至全程不需要打开本地的IDE。此举直接干掉了 github1s 这个具备类似功能的开源项目。
在浏览器写代码还能拥有桌面IDE的开发体验,是IT行业一直在追求的。VSCode的团队负责人Erich Gamma(JUnit作者之一,《设计模式》作者之一,Eclipse架构师) 在2011年入职微软后的工作内容就是:
Envision new paradigms for online developer tooling that will be as successful as the IDE has been for the desktop.
之后才有了VSCode的诞生。可以说VSCode从诞生之处就朝着云端IDE的方向去设计。
为什么要使用云端IDE?这源于本地开发环境的一些问题,比如:
环境差异问题:如Mac与Linux就有一些差异,尤其是在Mac上使用基于Linux内核的流行技术如Docker,会让整个体验变差。
性能问题:本地的开发机器一般需要运行很多软件,如一些办公和通讯软件啥的,让本来就不高的性能捉襟见肘。
稳定性问题:本地的开发机器启动时间长了就需要重启,每次重启后需要花费很多时间去设置相关的开发环境,浪费了大量时间。
依赖问题:如果开发环境依赖一些特定的云基础设施,网络通信就会很麻烦。而在云主机上,天然与其他云基础设施在一个网络环境中,环境设置简单。
网络问题:现代软件开发都是站在巨人的肩膀上,很多软件都依赖大量的库、框架和运行时,这些依赖下载需要快的网络速度。一般云主机的网络性能要好于家庭或办公网络。
安全问题:代码或者密钥放在本地开发环境有泄漏的风险,比如开发人员的开发机器被盗后丢失了代码或者密钥。
存储问题:本地开发机器的磁盘存储有限,扩展性也不好。而云主机的磁盘很容易扩容。
而解决这些问题的终极办法就是把开发环境搬到云端,而在云端开发最重要的就是需要一个好的IDE支持,这也导致了行业对云端IDE强烈的需求。
云端IDE
在说云端IDE前我们先了解下IDE的一些主要功能点,如下图所示:
一个优秀的IDE当然要让程序员写代码写的爽,看代码看的溜。而要达到这个目的,必不可少的要支持以下功能点:
优秀的文本编辑功能,支持键盘自定义布局;
代码提示功能,如语法高亮,代码跳转,错误提示等;
调试功能;
多编程语言支持;
代码补全功能;
代码重构功能;
扩展能力,支持用户自定义或提供插件;
好的生态。
在桌面级IDE里,这些功能都不是事,有很多IDE支持这些功能,比如Visual Studio、Eclipse、IntelliJ IDEA、NetBeans及Xcode等。但以支持在线使用的维度看,这些老牌IDE都不行。
早期行业里对云端IDE的要求也不高,所以有了大约三个大的类别的云端IDE,如下图:
在线编辑器。以CodePen与JSFiddle为主的Web在线编辑器,可以方便的对前端页面做在线开发。但这距离桌面IDE的体验相差甚远。
类REPL。以Repl.it和Jupyter为主的在线 REPL。REPL最多算桌面IDE支持众多功能中的一个点,它的使用场景适合一些编写一些验证类型的代码片段,距离工程化的代码开发体验还有很多路需要走。
功能受限的云IDE。以AWS Cloud9为主的云IDE,这类云IDE已经能给予很好的代码开发体验了,甚至可以无缝使用云基础设施,很适合规模化的代码开发协作。但这类云IDE一般不具备扩展能力,比如无法安装一些插件。
在上述功能受限的云IDE类别里面有一些基于VSCode而实现的云IDE,比如Github Codespaces使用了VSCode结合Azure云服务器可以给予开发者桌面IDE的体验,也可以安装插件。同时在最受欢迎的开发者社区StackOverflow的一份 2021 Developer Survey 调查问卷里的 Integrated development environment(IDE) 部分中(共八万多个开发者调查反馈),VSCode以71%的高票(2019年这个占比已经到了50%)当选最受欢迎的IDE:
看起来VSCode达到了云IDE想要实现的终极目标:与桌面级IDE一样的开发体验。这里的问题是为什么是VSCode呢?
为什么是VSCode
良好的设计
VSCode远程开发模式如下图所示:
通过CS架构设计让VSCode具备使用远程服务器或容器的能力,本地的VSCode只负责UI界面与主题的显示,其他的如插件、程序运行、终端进程与调试器都运行在远程服务器上。界面显示与计算分离的设计,这也是实现云IDE很重要的一点。
CS架构设计也体现在代码提示上,通过制定Language Server Protocol标准协议,VSCode核心无需解析多种编程语言的AST或实现多个编程语言Parser,而将这些功能委托给各语言的插件来实现,保证了核心非常小巧且稳定。
同样的设计也体现在了调试器(Debugger)与Debug Adaptor Protocol标准协议上。
更多的架构设计分析可以看我的这篇 VSCode插件开发小记。
跨平台
VSCode的前身是 monaco-editor 在线编辑器。作为一个Web软件,想要实现跨平台功能,就需要使用Electron技术。所以桌面级的VSCode和服务器端Web版的VSCode其实是一个代码库。
正因为其是一个Web软件,才有了 code-server 这个第三方Repo的出现,也才有了运行在浏览器里的VSCode。这才导致了众多基于VSCode的云IDE的出现。
开源
VSCode 如果没有开源,结局可能大不相同。正是有了微软华丽的转身,对开源的热情拥抱,才打开了VSCode走向世界的大门,否则结局可能是沦落为微软众多内部项目的一员。
基于AWS与Pulumi搭建个人云IDE
由于VSCode的开源和Web特性,让我们可以快速搭建一个基于VSCode的个人云IDE,功能对标 Github Codespaces,但比其便宜很多。
我的实现见这个 Repo。架构如下:
前置条件:
需要一个AWS账号且安装AWS CLI,本地配置好AWS Credentials。 AWS账号需具备对EC2的一些操作权限;
需要一个 Pulumi 账号,并创建一个工程;(如果你对Pulumi不熟悉,可参考我这篇 基于Serverless实现静态博客访问统计功能 文章里的介绍)
使用非常简单(得益于Pulumi和AWS CLI的强大功能):
git clone https://github.com/bmpi-dev/co…
cd code.bmpi.dev/server
pulumi up # 使用 Pulumi 设置 AWS EC2
./run work # 打开远程 VSCode
./run rest # 关闭远程 VSCode
如果./run open_tunnel因为服务器还在启动时通道无法连接,可在服务器启动后再次执行即可建立通道连接。
首次访问需要输入VSCode的登录密码,通过执行sh connect-server.sh登录服务器后执行cat ~/.config/code-server/config.yaml | grep password:获取登陆密码。
通过浏览器访问http://localhost:8888/后即可开始使用远端VSCode:
如果暂时不需要这个环境了记得通过./run rest休眠这台云服务器,服务器在关闭后AWS就不对该EC2实例计费了,只对存储卷收取很便宜的费用。
如果你彻底不需要这个环境,想销毁所有资源免得AWS继续收费,只需要执行pulumi destroy即可删除所有AWS资源。
远程访问
服务器因安全原因一般只会开启特定端口供客户端远程访问。Code Server只开放了 8888 端口,但这对开发来说很不方便。我们在开发过程中,经常需要不同的端口来测试,比如通过多个端口连接多个Web Server或中间件服务等。
一种解决方案是通过临时端口转发来允许远端客户端访问,但这种方法需要不停的修改服务器防火墙配置,不是很方便。
另外一种方案可以通过建立客户端和服务器的VPN网络,以使客户端和服务器在同一个虚拟网络之中。我用 tailscale 来实现搭建一个本地电脑和远端服务器间的VPN网络。一旦配置好tailscale后,Code Server就变成本地电脑可以直接访问的服务器了。同时安全性也有保障。
关于费用
以AWS EC2 T2.Medium实例(2核4GB内存+ 50GB存储)为例。每天开发5小时,每月20天共100小时,总成本为\(0.0464 * 100 + \)
0.1 * 50 = $9.64。相同的服务器配置Github Codespaces需要21.5美元,相差近2.23倍。
云IDE研发模式
云IDE代表了一种研发模式的发展方向。这种研发模式可能的发展趋势有:
标准化的开发环境:一套云IDE开发环境可以标准化批量配置,开箱即用,大幅度降低开发者在配置开发环境上的时间消耗。
定制化的开发环境:可以对开发环境做定制,满足不同类型项目的开发环境需求。
弹性的开发环境:开发环境的配置依托于云服务的自动弹性扩容,可动态调整配置,满足开发环境对资源配置的动态需求。
智能的开发环境:依托云服务器对特定代码仓库的机器学习分析,能够更好的做到智能提示,辅助开发,类似于 GitHub Copilot。
安全的开发环境:代码和基础设施配置都在云服务器上存储,可以极大的降低因开发人员疏忽而造成的代码或者环境密钥泄露的风险。只要做好云服务器的系统安全配置,就可以降低研发环境的安全风险。
随时可用的开发环境:无需特定开发机器,只需要一个具备浏览器的电脑即可访问云IDE开始开发。
一句话总结就是:
Cloud IDE, Coding Anytime Anywhere.
参考文章
从VSCode看大型IDE技术架构
Implementing VSCode-based (Code-Server) on Cloud with AWS CDK
更新时间: 35个月前 版本: 8171f612c
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
基于Serverless实现静态博客访问统计功能
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
K8S云原生应用开发小记
8 November 2021 27:15 | 5997 字 | 10984 次阅读
什么是X
kubernetes • K8S • Cloud Native • 云原生架构
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
注:本篇文章是作者与 GitHub Copilot 结对创作完成。Copilot大概完成了本文5%左右的部分。作者也在这个 Tweet 上部分记录了Copilot的创作部分。
作为一个业余独立开发者,我在开发上的投入时间和资金都不多,所以对 项目技术栈 的选择有两个很基本的要求:
这项技术能极大的提高我的开发效率。
这项技术不需要投入过多资金。
第一点对我的影响是,我会选择使用更高效的技术栈,包括编程语言、生态与架构等:
我对 Elixir 的尝试,就是因为它是基于Erlang平台的,有着强大的并发模型与富有表达力的语言特性,还可以利用Erlang/OTP的生态,这可以让我更加高效的开发上线某个产品。
我对 Serverless 的尝试,是因为不少个人产品初始用户都很少,用传统租VPS的方式会浪费很多资源,而且Serverless的弹性扩容能力与可用性都是传统VPS不可比拟的,所以我会用Serverless开发一些小产品。
我对 IaC 的尝试,是因为它可以以声明的方式构建基础设施,还可以对基础设施版本做管理,这样我对基础设施的投入是一次性的,而无需每次重复手工操作配置部署环境。
第二点对我的影响是,我会避免使用庞大耗费资源的技术,转而去寻找廉价轻量级的替代技术。这也意味着我不会追求过高的SLA,性价比是我的主要目标。选择工业级的技术栈,牺牲掉一定的可用性,一方面让我可以接受这个成本,另外一方面还能得到工业级的扩展性。这方面的体现是我对Serverless的成本分析上,我会选择成本可以承受的服务组件,同时选择最具性价比的计费方式。
我对K8S的初步印象是它并不符合我这两点的要求。这也是我一直没有在个人项目上尝试的原因。直到我看到这篇 The Architecture Behind A One-Person Tech Startup 的长文。
作者介绍了他在个人项目上运用K8S的经验,K8S虽然需耗费一定的资源,但却带来了强大的扩展性与可靠性。
架构演进历史
应用架构从组成形态来看,主要分为单体与分布式架构:
原始的分布式架构甚至比单体都要早,因为早期的计算机性能都很差,无法满足人类不断膨胀的对计算能力的需求,进而导致应用架构的演进。随着单台计算机性能的提高,而原始分布式技术又非常的复杂,所以单体架构流行了很长一段时间,直到单台计算机的性能无法满足计算人类社会爆炸增长的海量信息。
分布式架构能协调利用多台计算机的计算能力,提供更高的性能,更高的可用性,更高的扩展性。但由于其复杂性,分布式架构的演进又分为这几个阶段:
第一阶段:面向服务的架构(SOA)。面向服务的架构是一次具体地、系统性地成功解决分布式服务主要问题的架构模式。但这种架构需要应用开发人员花费大量的时间和精力学习框架本身,且这种架构模式的架构设计比较复杂,推广成本太高。
第二阶段:微服务架构。微服务是一种通过多个小型服务组合来构建单个应用的架构风格,这些服务围绕业务能力而非特定的技术标准来构建。这就解决了SOA的复杂性问题,让业务开发人员能更专注业务开发。但微服务的问题在于业务开发人员依旧要面对这些注册发现、跟踪治理、负载均衡、传输通信等分布式架构需解决的问题。
第三阶段:云原生架构。云原生架构是从软件层面独力应对微服务架构无法解决的问题,发展成为软、硬一体(软件定义计算、软件定义网络、软件定义存储),合力应对分布式架构的通用问题。利用容器、虚拟化技术、不可变基础设施、服务网格、声明式API等技术,K8S提供了开箱即用的弹性伸缩、服务发现、配置中心、服务网关 、负载均衡、服务安全、监控告警、容错处理等功能。这些技术能够构建容错性好、易于管理和便于观察的松耦合系统,结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。
云原生时代
当虚拟化的基础设施从单个服务的容器扩展至由多个容器构成的服务集群、通信网络和存储设施时,软件与硬件的界限便已经模糊。一旦虚拟化的硬件能够跟上软件的灵活性,那些与业务无关的技术性问题便有可能从软件层面剥离,悄无声息地解决于硬件基础设施之内,让软件得以只专注业务,真正“围绕业务能力构建”团队与产品。(摘自 凤凰架构/后微服务时代 )
Kubernetes(K8S)
设计理念
K8S创造了一种DSL的语言,用户通过这种语言声明式的定义了分布式系统架构中用到的一切资源(如计算、网络、存储、路由、密钥、证书)。当用户定义了自己期望的资源状态,K8S就会自动帮助用户创建这些资源,并且自动管理这些资源。
资源在Kubernetes中是极为常用的术语,广义上讲,Kubernetes系统中所有你能够接触的方方面面都被抽象成了资源,譬如表示工作负荷的资源(Pod、ReplicaSet、Service、……),表示存储的资源(Volume、PersistentVolume、Secret、……),表示策略的资源(SecurityContext、ResourceQuota、LimitRange、……),表示身份的资源(ServiceAccount、Role、ClusterRole、……),等等。“一切皆为资源”的设计是Kubernetes能够顺利施行声明式API的必要前提,Kubernetes以资源为载体,建立了一套同时囊括了抽象元素(如策略、依赖、权限)和物理元素(如软件、硬件、网络)的领域特定语言。通过不同层级间资源的使用关系来描述上至整个集群甚至是集群联邦,下至某一块内存区域或者一小部分的处理器核心的状态,这些对资源状态的描述的集合,共同构成了一幅信息系统工作运行的全景图。(摘自 凤凰架构/不可变基础设施 )
优势
能够构建容错性好、便于观察的应用
能够以统一的方式管理应用
让应用具备弹性扩容的能力
一键应用安装部署(Helm)
劣势
资源成本高。不论K8S的Master或者Worker节点都需耗费一定的计算资源。
学习成本高。K8S重新定义了很多抽象的技术概念,使用门槛不低。
云平台托管K8S成本分析
我对不同云平台的Kubernetes托管方案选择主要是基于成本方面的考虑。这篇 K8S Cluster Cost Compare 文档提供了不同云平台(AWS/Azure/GCP/DigitalOcean/Vultr)的Kubernetes托管方案的成本比较。
我最终选择了最便宜的DigitalOcean云平台,Master控制平面基础版(对SLA没有高的要求就可以用)是免费的,Worker节点是新加坡区域的2核4GB内存的机器(\(20/月),还有一个\) 10/月 的Load Balancer费用。一个月总成本是$30/月。
由于Worker节点需要安装一些K8S本身的服务如kube-proxy、core-dns等共计12个pod,占用Worker节点一半的内存。这样供应用调用的资源还剩2GB。
应用开发部署与架构
以上图展示了这个云原生应用的开发部署流程以及K8S部署的各内部服务的架构:
开发部署流程。代码推送到GitHub后会触发两个动作:
Vercel会检测前端代码的变化,如果有变化就自动部署到Vercel的CDN上。
GitHub Actions会检测后端代码的变化,如果有变化就自动构建镜像并发布至GitHub Packages里,然后自动创建一个新的K8S Deployment,并重新部署后端服务。
请求流处理流程。当一个用户访问网站时,DNS经Cloudflare解析后,浏览器会发送两个请求至:
Vercel端:浏览器拉取静态页面资源。
K8S端:请求经K8S的Load Balance解析Ingress规则后转发至default Namespace的ExternalName类型的Service,之后经其转发至后端服务 (Namespace为free4chat) 的Service,经Service最终转发至其中某个Pod的Container。Container中就是我们的后端业务应用。
实现
最终效果见:线上版本。
源码见:代码仓库。
前置条件
前置知识
如果你对K8S不了解,可以先看这个高质量的入门视频:Kubernetes Tutorial for Beginners [FULL COURSE in 4 Hours]。在实际操作之前,确保自己能理解基本的K8S概念:Namespace、Deployment、Service、Pod、Node、Ingress。
前置资源
你需要先注册以下账号:
DigitalOcean
Cloudflare
一个域名。
并安装这些软件:
kubectl
doctl
helm
工程目录结构
.
├── .github
│ └── workflows
| └── workflow.yml
├── Makefile
├── backend
├── frontend
└── infra
├── Dockerfile.backend
├── k8s
│ ├── free4chat-svc.yaml
│ ├── ingress-free4-chat.yaml
│ ├── ingress_nginx_svc.yaml
│ └── production_issuer.yaml
└── tools
└── nsenter-node.sh
整体项目分为frontend、backend与infra部分,本文主要聚集于infra部分,也就是K8S的部署。infra部分没有使用IaC,因为K8S本身就是声明式的构建文件,如果不使用一些云托管服务的话,没必要增加复杂度去使用IaC。CI/CD部分使用GitHub Actions完成的。
Dockerfile
后端服务是个Golang应用,打包的 Dockerfile 在此。我还做了一个 Makefile 的简单配置,用于编译后端服务。本地使用Docker部署后端服务可以使用这个 Makefile。
配置K8S
创建K8S Cluster
在DigitalOcean创建K8S Cluster是一件非常简单的事情,你只需要选择好区域(取决于你的业务用户所在区域)与Worker Node的规格(取决于你的成本预算)就可以创建出一个Cluster。
连接K8S Cluster
使用doctl配置K8S:
doctl kubernetes cluster kubeconfig save use_your_cluster_name
Namespace
Namespace是K8S用来在单个集群中隔离资源组的机制。比如我们可以在同一个集群创建不同的业务Namespace,而这个Namespace下存放着和这个业务相关的Pod、Service、Deployment等资源,如果要删除这个业务相关的资源,只需要删除这个Namespace即可。
K8S默认有个kube-system的Namespace,这个Namespace下存放着和K8S相关的资源。还有一个default的Namespace,这个Namespace存放默认(不带Namespace)被创建的资源。
Backend Service
先创建一个后端服务的Namespace:
kubectl create namespace free4chat
再创建一个后端Service模版 free4chat-svc.yaml:
apiVersion: v1
kind: Service
metadata:
name: apifree4chat
spec:
ports:
apiVersion: apps/v1
kind: Deployment
metadata:
name: apifree4chat
spec:
selector:
matchLabels:
app: apifree4chat
replicas: 1
template:
metadata:
labels:
app: apifree4chat
spec:
containers:
- name: echo
image:
resources:
requests:
memory: “128Mi”
cpu: “250m”
limits:
memory: “500Mi”
cpu: “1000m”
ports:
- containerPort: 8888
之所以说这是模版,因为在image这块放了一个
这个模版定义了一个Deployment和Service资源。Deployment定义了Pod实例的CPU和内存限制、实例数、端口映射和容器镜像等资源。Service定义了Cluster内部访问后端服务的域名和端口。
最终GitHub Actions会将这个Service和Deployment部署到K8S Cluster中的free4chat的Namespace中。
Ingress Controller
有了后端服务的Service,如何让外部流量进入后端服务?这是K8S Ingress做的事情。我们首选要安装Ingress Controller,它有很多类型,如HAProxy、Nginx、Traefik等,我们这里选择Nginx。在DigitalOcean K8S管理界面上找到 Nginx Ingress Controller,然后点击安装即可。
这样会自动创建一个ingress-nginx的Namespace,并且会创建一个DigitalOcean Load Balance的服务,这个服务费用是$10/月,有独立的IP地址(可在DigitalOcean管理界面查看)。之后我们在DNS配置的时候会用到这个IP。
现在我们需要在default Namespace下创建一个ingress规则,将LoadBalance的流量转发至后端服务,这个配置文件是 ingress-free4-chat.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-free4chat-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod # 这个是 cert-manager 的 ClusterIssuer,用于自动生成 SSL 证书
spec:
tls:
kind: Service
apiVersion: v1
metadata:
name: apifree4chat
spec:
type: ExternalName # 因为后端服务不在 default Namespace 中,需要通过 ExternalName 这个 Service 来转发至 apifree4chat Namespace 中的后端服务
externalName: apifree4chat.free4chat.svc.cluster.local # 跨 Namespace 的后端服务域名
这个配置文件会生成两个资源,一个是ingress规则,一个是ExternalName类型的Service。我们会在配置好cert-manager的ClusterIssuer后,使用kubectl来创建这个资源。
Cert Manager(HTTPS)
在K8S中通过Cert Manager来自动生成和更新域名的SSL证书,在这里面我们使用Let’s Encrypt服务来为我们颁发证书。
先通过Helm一键安装Cert Manager应用:
kubectl create namespace cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager –namespace cert-manager –version v1.2.0 –set installCRDs=true
执行完这些命令后需要创建一个为生产环境颁发SSL证书的ClusterIssuer资源,配置文件是 production_issuer.yaml:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
# Email address used for ACME registration
email: your@email.com
server: https://acme-v02.api.letsencry…
privateKeySecretRef:
# Name of a secret used to store the ACME account private key
name: letsencrypt-prod-private-key
# Add a single challenge solver, HTTP01 using nginx
solvers:
- http01:
ingress:
class: nginx
在DigitalOcean中,为了让Cert Manager能够自检,必须通过Nginx Ingress Controller启用Pod-Pod通信,这样Cert Manager才能正常工作为K8S颁发证书,创建一个Service资源,配置文件是 ingress_nginx_svc.yaml:
apiVersion: v1
kind: Service
metadata:
annotations:
service.beta.kubernetes.io/do-loadbalancer-enable-proxy-protocol: ’true’
service.beta.kubernetes.io/do-loadbalancer-hostname: “k.free4.chat”
labels:
helm.sh/chart: ingress-nginx-2.11.1
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/version: 0.34.1
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/component: controller
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
type: LoadBalancer
externalTrafficPolicy: Local
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
- name: https
port: 443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/component: controller
创建资源
经过以上步骤我们有了一些声明式的K8S资源创建配置文件了,现在是时候真正开始创建这些资源了:
kubectl apply -f production_issuer.yaml # 创建颁发 SSL 证书的 ClusterIssuer 资源
kubectl apply -f ingress_nginx_svc.yaml # 创建解决 Pod-Pod 通信的 Service 资源
kubectl apply -f ingress-free4-chat.yaml # 创建 ingress 规则资源
当执行完这些命令后,所有环节里我们还缺少:
后端服务资源的创建。这个我们会通过GitHub Actions来创建。
DNS域名配置。这个我们会在Cloudflare上配置。
DNS配置
在Cloudflare上配置DNS解析:
由于我们前端部署在Vercel上,后端在K8S上,所以这里采用了给后端K8S创建一个*.k的A类记录,映射的IP就是Digital LoadBalance的IP。而给主域名的A记录映射到Vercel。这么做的原因是我们没法同时给一个主域名创建两个不同的IP记录,所以只能给后端子域名的方式来解决这个问题。
最后创建两个CNAME记录,分别是:
api -> api.k.free4.chat:我们的后端服务API域名。
www -> www.free4.chat:我们的主域名。
到了这一步我们就可以访问 https://www.free4.chat 了。但 https://api.k.free4.chat 还不行,因为后端服务并没有创建完成。所以接下来需要通过GitHub Actions来创建后端服务。
GitHub Workflow
通过GitHub Actions来创建后端服务的好处是让开发部署自动化,当后端代码产生变化时,会自动触发GitHub Actions来构建新的镜像并创建新的后端服务。
要创建一个GitHub Workflow,只需要创建 .github/workflows/workflow.yaml:
name: DO_K8S_Deploy
on:
push:
branches:
- main
paths:
- ‘backend/src/’
- ‘infra/Dockerfile.backend’
- ‘.github/workflows/’
jobs:
build:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
- name: Checkout master
uses: actions/checkout@main
# Install doctl.
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
# Build a Docker image of your application in your registry and tag the image with the $GITHUB_SHA.
- name: Build container image
run: docker build -t ghcr.io/madawei2699/apifree4chat:$(echo $GITHUB_SHA | head -c7) -f ./infra/Dockerfile.backend .
- name: Log in to GitHub Packages
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image to GitHub Packages
run: docker push ghcr.io/madawei2699/apifree4chat:$(echo $GITHUB_SHA | head -c7)
- name: Update deployment file
run: TAG=$(echo $GITHUB_SHA | head -c7) && sed -i 's|<IMAGE>|ghcr.io/madawei2699/apifree4chat:'${TAG}'|' $GITHUB_WORKSPACE/infra/k8s/free4chat-svc.yaml
- name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 ${{ secrets.CLUSTER_NAME }}
- name: Deploy to DigitalOcean Kubernetes
run: kubectl apply -f $GITHUB_WORKSPACE/infra/k8s/free4chat-svc.yaml -n free4chat
- name: Verify deployment
run: kubectl rollout status deployment/apifree4chat -n free4chat
这里唯一要做的就是提前给这个repo的Actions secrets里创建 CLUSTER_NAME 与 DIGITALOCEAN_ACCESS_TOKEN 的环境变量供GitHub Actions使用。其中 DIGITALOCEAN_ACCESS_TOKEN 就是DigitalOcean的API Token,而 CLUSTER_NAME 就是我们的DigitalOcean上的Kubernetes Cluster的名字。
这样每当代码有更新被推送到GitHub时,一个新的服务(包括前后端)会自动构建并发布到Vercel和K8S上。
这时候我们的应用就上线了!
还需要做的事情
日志:传统的ELK需要大量的服务器资源,不适合我们这种轻量级的集群。最简单的就是跑到多个Pod里看日志,这方面有个 stern 的工具可以帮助我们在多个Pod间进行日志查询。
监控与告警:我们可以通过安装Prometheus和Grafana的方式来监控我们的服务,并且可以通过Prometheus的Alert Manager来发送告警。但如果整个集群都挂了,那安装到集群里的监控告警服务也不会有效果,所以最佳的实践是使用外部的监控告警服务。这方面可以使用 New Relic 或类似的服务来实现。
错误追踪:集成 Sentry 就可以实现后端服务错误追踪。
总结
到这里我们已经从零开始(没有包括K8S Master控制平面)构建了一个K8S集群。让我们思考一个问题,那就是K8S到底帮助我们解决了什么问题?
让我们先思考下现代软件开发经常要考虑的 12要素 :
以上的这些要素,K8S都直接或间接给出了它的解决方案,而K8S也和围绕它的生态让工程师能以低成本去构建出满足这些软件设计要素的健壮软件。
我想这也是K8S能号称云上操作系统的原因吧。
参考资料
Modern Web Hosting for Personal Projects - Mike Cartmell’s blog
Architecting Applications for Kubernetes | DigitalOcean
Kubernetes: The Surprisingly Affordable Platform for Personal Projects | doxsey.net
Save Money and Skip the Kubernetes Load Balancer: Lowering Infrastructure Costs with Ingress Controllers, DNS, and Host Ports|downey.io
DigitalOcean Kubernetes Without a Load Balancer - Mike Cartmell’s blog
BMPIGitHubTwitter学习技术投资|English
AI降临
6 December 2022 5:49 | 1281 字 | 5432 次阅读
chatGPT • GPT-3 • Midjourney • Copilot • AlphaGo
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
最近无论我的朋友圈、微信群还是社交媒体,都能看到很多人分享chatGPT的魔力,似乎它不是一个程序,而是一个有智慧的物体。更有人用《2001太空漫游》电影中的一个名场面来类比:
只不过电影中的黑石碑此刻变成了chatGPT。这种场景上次出现还是在AlphaGo战胜李世石的时候,当时很多人对人工智能在如此短时间战胜人类顶级棋手的能力感到震惊,不过也仅限于围棋这种与日常生活无关的领域。而chatGPT的出现,让人工智能的能力进一步扩展到了日常生活工作中,这种能力的扩展,让人感到更加震惊。
在聊chatGPT之前,让我来先介绍下我之前使用的一些AI模型:Copilot与Midjourney。Copilot是GitHub开发的一个机器学习模型,主要用于代码提示,我在之前的一些文章里也介绍过,当时它的表现已经让我很震惊了,除了能根据上下文写一些简单的代码外,它还可以写文章:
GitHub Copilot
GitHub Copilot
当然这种震惊还有期望小的加成,我内心只是把它看作一个简单的辅助工具,它的代码提示功能相当于是一个智能的code snippet工具,虽然能写文章却是让人有些惊奇,但它也只能做一些简单的语句扩充(这种语句扩充是根据上下文而处理的,所以有时候它会表现的很聪明)。
而Midjourney的出现的确让一些人失业了,通过一些简单的文本描述就能产生与其高度关联的画作,这种超能力之前只出现在人类身上。目前有超过五百万的用户时刻在与Midjourney的聊天机器人互动去创作新的画作,比如当给Midjourney一些提示语时,它能在不到一分钟内帮你创作出如下画作:
这让人不得不思考,当AI具备了这些创作能力后,未来的电影、游戏等领域也逐渐会被AI介入,最终一部电影的创作人员全部是AI也是很有可能的。
chatGPT是什么?这个问题让它自己来回答显然更合适:
要在一篇文章里展示chatGPT的惊人能力实在太难了,最好的方式就是去注册一个账号去与它聊天,去感受它的智能。当然以下的一些场景都是让人惊叹的。
比如用chatGPT来刷Leetcode:
用chatGPT回答知乎问题:
与chatGPT聊一些有意思的话题:
用chatGPT了解一些法律知识:
用chatGPT解决编程问题:
用chatGPT模拟运行数据库:
用chatGPT模拟运行程序:
当然最让我震惊的是,它能像一个软件工程师一样与我结对编程,解决实际的编程问题:
还能用TDD的方式来编程:
当然能通过持续的对话去了解需求,根据需求拆分任务,说人话就是它能像人类一样去持续的解决问题,比如它帮我解决了AdventOfCode 2022 Day 1的谜题,一开始用Python写的,最后用Elixir重写了一遍,当然是秒级的速度:
根据我的测试,chatGPT起码是一个入门级的程序员的水平,而且是全栈全语言的全能选手,某些程度上堪比高级程序员。当然这只是我个人的感受,不代表它的实际能力。它到底有多强,我也不知道,很多时候并不是它不能解决问题,而是可能我没有给它提出一个好问题。
这种感觉很像《降临》电影中给人的观感:你看到它就在哪里,你并不了解它,你也不知道它会给你带来什么。
更新时间: 30个月前 版本: a99084a6f
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
AI驱动开发:从Prompt到Product(直播)
我与ChatGPT结对编程的体验
ChatGPT背后的语言模型简史
ChatGPT应用开发小记
我的AI阅读助手
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我的AI阅读助手
26 March 2023 6:49 | 1503 字 | 22459 次阅读
chatGPT • 信息输入 • 自我提升 • 知识管理
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
AI降临
今年你一定或多或少被chatGPT等AI模型的消息轰炸过。早在去年年底的时候我就被chatGPT的实力震撼过,为此还专门写过一篇文章:AI降临,虽说AI模型是被人工训练出来的,但许多测试都表明它或多或少都有一些与人相似的智能,根据算力的演进速度,它的成长可能比很多人想象中的要快,所以所今年是AI降临年也不为过。如果你经常混迹推特,可能对此会更有体会,每天有数不尽的AI研究被公布,新的模型被发布,甚至GPT-4也被OpenAI发布了,而chatGPT只是GPT-3.5的一个调优版!
虽然2023年是糟糕的一年,很多公司在裁员,经济比大家预期的要差。但AI技术的突破性成果让这一切看起来像是iPhone发布的初期,iPhone在2008年发布,当年正是全球次贷危机爆发的一年,而且PC的发展在当时看也走到了尽头,是iPhone带领我们走向了移动互联网蓬勃发展的十年,谁也没能预料到iPhone给我们带来生活上的多大改变。所以乐观点看,也许2023年未来十年不错的开端,就算你此刻的处境也很糟糕,希望能坚持向前看,相信未来会更好。
关注我的朋友都知道,我投入很多的精力在知识管理上,也写了一些和信息获取的文章如构建高质量的信息输入渠道与构建自己的信息简报。这段时间我深深的意识到AI将会对我们未来的学习产生很深远的影响,从chatGPT能自由的切换语言到瞬间整理总结知识,这一切都看起来是如此的震撼。我为此专门测试了一些让chatGPT帮助我阅读总结整理书籍、文章、代码仓库,甚至与它结对编写代码。
myGPTReader
在做完这些测试后,我意识到是时候革新我的知识管理流程和工具了,第一步就是从获取信息的阅读作为切入点,所以我花了十几天时间与chatGPT合作开发了我的个人AI阅读助手:myGPTReader,它是一个在slack聊天软件里的bot,可以读取任何网页、电子书与文档,并根据与问题相关的内容做总结与分析处理,无论你给它的是什么语言的版本,它都能按你需要的语言去整理,这对平时阅读大量英文文章的我来说是一个效率的提升,当然这个bot背后还是chatGPT,所以可以询问它任何问题。
它能做什么?
功能特性
在线读取任意网页内容包括视频(YouTube),并根据这些内容回答你提出的相关问题或总结相关内容。
支持读取电子书与文档(支持PDF、EPUB、DOCX、Markdown、TXT),并根据这些内容回答你提出的相关问题或总结相关内容。
定时发送每日热榜新闻,无论新闻是中文还是其他语言,它都能使用chatGPT用中文自动总结新闻的内容,方便快速获取热点新闻信息。
支持 prompt 模版,能根据消息历史记录的上下文回答你的问题,甚至能和你玩游戏。
支持多国语音交互(英文、中文、德语与日语),它会根据你的语言使用相关语言的声音来回答你的问题,从而帮助你训练外语能力,可以理解为它是你的私人外教。
这是一个开源软件,意味着你可以私有部署它到你的workspace中,当然也可以加入我的 slack 频道来免费体验它的强大。
AI时代的学习范式
当AlphaGo战胜了人类顶级棋手李世石那一刻,人类认为最复杂的棋类被AI淘汰了,当Stable Diffusion能够在一秒内画出人类画师一天的作品时,人类认为最复杂的艺术领域开始被AI攻克,当chatGPT能够在一秒内回答人类的任何问题时,人类认为最复杂的知识领域开始被AI攻克。
如果AI能够迅速掌握很多我们人类需要数年掌握的知识,那学习的意义何在?
如果把现今人类的知识领域看作冷兵器时代,那chaGPT的出现就如枪的诞生一样。一个带枪的人与冷兵器时代的人,谁更有优势?这个带枪的人就是超级个体,他能快速去实现自己的想法,而不需要去学习那么多细节性的知识,参加那么多无效的沟通会议,他能在短时间内实现以前一个团队才能实现的事情。
如果我们的很多技能注定要被AI淘汰的话,最好的就是提前去与它共舞。当第一辆汽车问世的时候,很多马车夫不以为然,但也有人主动去学习它,去迎接一个新时代的来临。
更新时间: 26个月前 版本: 4a556e913
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
构建高质量的信息输入渠道
构建终身学习体系进行自我提升
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我的2023
3 February 2024 21:25 | 4712 字 | 9607 次阅读
年度总结
年度总结
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
又是一年过去了,2023年我完成了一些重要的事情,也有一些事情没有完成。这篇文章是我对2023年的个人总结,在这里我不讨论工作,不讨论大事,只分享一些个人的小事。
年度关键词
我想通过一些关键词来概括我的2023年对我影响最大的一些事情,其实总共也就是两个大的方向。
AI
2023年是我在AI领域上投入的应该是最多的一年,比如学习AI相关的技术,用ChatGPT来写作及开发产品,以及一些AI产品的开发。在IT行业里,AI也是最火的一个方向,很多公司已经用大语言模型来改造他们已有的产品。
我在AI领域输出的内容有:
我的AI阅读助手
ChatGPT应用开发小记
ChatGPT背后的语言模型简史
我与ChatGPT结对编程的体验
还有一个比较火的开源项目:myGPTReader,一些相关的直播演讲和给i365会员录制的视频。
当然最重要的是我还开发了几款个人AI产品。
出海
2023年我开始了一些出海的尝试,也在尝试寻找一些出海的机会,但是存在一些困难。于是只能退而求其次,先将被动收入投资组合的资金转移到美股市场。我还开设了多个美股账户,及香港、新加坡、德国及美国的银行账户,现在可以说是全球化投资了。^_^
当然在个人产品方面,我也在尝试一些出海的机会,比如myGPTReader,Chat2Invest。
在内容输出方面,也尝试在LinkedIn上不定期发些帖子,不过效果不太好。因为语言、文化及习惯的差异,用另外一种语言建立影响力是挺难的事情,能跟随上就不错了。
影响力
2023年我在影响力打造上投入的时间明显降低了,原因自然也是因为我在AI与出海上投入了过多的精力,也没多少时间写文章,甚至在推上更新的也很少,不过写作会是我长期坚持的事情。我希望在2024年在影响力上继续保持一定的输出。
2023年博客的总访问量有317K,总访客量有105K。数据上较上一年下滑了一些,但考虑到在内容写作上明显减少了,这个数据还是可以接受的。
其他平台的一些数据有:
Twitter:粉丝数28.3K,推文数4K。
公众号(improve365):关注数5.7K。
知乎:14K。
即刻:1.5K。
邮件订阅:1.8K。
这些平台除了Twitter的增长较快外,其他平台因为我并没有认真去经营,所以增长的不多。
个人产品
2023年做的产品都和AI相关,其中有些是全新的,有些是老产品重新用AI技术进行了改造。
myGPTReader
myGPTReader是一个Slack机器人,它可以阅读任何网页、电子书、视频(YouTube)或文件,并通过ChatGPT进行总结。它还能通过语音与你交谈。
这个产品是一个开源项目,也是一个商业产品。它是我的第一款完整的个人产品,因为之前我做的产品都是一些小工具,或者是一些开源项目,并且不考虑商业化,所以这个产品对我来说是一个新的尝试。
这个产品的总用户数有8.2K,其中付费用户大概不到几十人,大多数用户都是尝鲜,因为这个产品出现的时机比较巧,刚好赶上AI的浪潮,所以吸引了很多人来尝试。但从功能来说,它只是对ChatGPT的一个封装,在细节上做的也很粗糙,以至于后面这个产品的用户留存率很低。后期我也没有过多的投入时间,所以这款产品一只脚已经踏入了坟墓。但我暂时还没有放弃它,因为我还有一些想法,所以今年如果有多余的精力,我会尝试改造它的功能。
虽然从商业上来说这个产品并没有成功,但它对我来说是一个很好的尝试,我学到了很多东西。
我的投资助手
我的投资助手是一款旨在帮助用户构建自己的交易系统的计划。它包括了Chat2Invest这个AI投资助手,以及多种投资交易策略的提醒工具,帮助用户评估交易能力的投资组合管理工具,以及一个专属的知识星球社区,用于分享投资知识和经验。无论是新手还是有经验的投资者,都可以通过“我的投资助手”来制定自己的投资计划,更加理性地进行投资决策,实现财富增值的目标。
这个产品的历史能追溯到2015年。当时正是万众创业的时代,我当时和人合伙创建了一个金融服务公司,因为做的是投资交易的服务,于是积累了一些投资方面的失败经验。于是在痛苦的反思总结后,我开始尝试建立自己的交易系统,最初只是一个用VBA开发的Excel模版,用来评估我当前投资组合持仓的风险。
在2016年时,因为创业失败在家失业蹲了半年后,我把产品包装成一个交易日记的App,这算是这个产品的第一代原型。但最后因为我找到了工作,同时因为我当时的技术非常菜,开发的后端性能根本满足不了产品的需求,几百个用户就把服务器搞崩溃了,所以这个产品也就搁置了。
后来到了2020年,我的技术水平有了一定的提升,还是基于这个交易的理念,我重新开发了这个产品,这次是一个交易策略的邮件提醒工具,而且是开源的:Invest Alchemy,在2022年的时候,我又加入了模拟组合回测评估的功能,这个工具的用户数有不到1K,但因为我没有商业化,所以没有收入。
时间到了2023年,在发布完myGPTReader后,我对产品开发的流程有了重新的认识。在ChatGPT的帮助下,于是重新把这个产品转型成了现在这个产品,一个拥有AI投资助手、支持多交易策略与组合评估的投资组合管理工具,以及一个专属的知识星球社区。目前这个产品的付费用户近20人,且大多数是终身会员。
从最初的一个失败的想法开始,到现在这个产品,一晃八年过去了,庆幸的是我还在坚持去实现当初的想法。希望今年能把这个产品的主要功能完成了,这是我今年的一个小目标。
这个产品最有意思的一点是,90%的代码都是ChatGPT 4生成的,虽然功能的复杂度已经非常高了,但ChatGPT依旧能实现我绝大部分想法。仅凭这一点,我相信未来已来,尤其是IT行业会出现很大的变革,我希望我这个马车师傅能转变成汽车司机。
i365会员计划
这个会员计划算是我在知识付费上的一个尝试,免费的内容我写了很多了,但是付费的内容我一直没有尝试过。这个会员计划是我在2023年开始的,目前有近三十多人。本来想在知识星球上做,但因为一些限制,我还是开发了一套自己的会员系统,有会员专栏、邮件推送、微信群等。
在内容方面,2023年我完成了个人产品开发指南,总共十几期,内容是关于个人产品开发的一些经验分享,包括产品的构思、开发、推广、运营等方面。这个系列文章是我在2023年的重要输出。在2024年计划继续输出一些关于AI产品开发的内容专栏。
其他项目
文生图:这是一个用AI驱动通过文本生成图的工具,是我和人合伙去做的一个项目。一方面可以让我去探索一些AI技术,另外一方面也可以学习下成熟产品的商业化运作。
myWriter:这是一个ChatGPT套壳的项目,没有任何技术含量,全靠渠道的客户来源,我只需要处理技术的问题,目前有几十个用户。
myFeeder:这是一个我虽然没有开发,但已经在构思的一个产品,希望今年有时间开始,因为域名我都买了。
生活
生活方面,2023年我没有太多的变化,依旧是在家宅的多,也没太多机会去旅游,希望2024年能有机会出去走走。
财务
在收入支出方面,因为我有记账的习惯,所以这个方面我还是比较清楚的。2023年家庭收入大概增加了三成,主要是因为在美股市场的投资收益,还有产品和项目的一些收入。在支出方面没有变化,虽然没能节流,但是也没什么新增的大开销。
被动收入投资组合在美股的收益大概是16%左右,这个收益率在美股市场显然不算什么,主要原因是投资的资金是分批出到美股市场的,再加上我对美股市场的了解还不够深,所以投资的收益率不高。不过我的投资助手已经开发出一些美股的交易策略,希望能在2024年有更好的表现吧。
健康
今年最多的运动就是和同事们一块打羽毛球,甚至形成了一定的习惯。但因为吃的更丰富了,所以体重还是在飙升。我还买了Switch的健身环,但显然没什么卵用,希望2024年能有更多的运动,体重能控制在一个合理的范围内。
阅读
今年感觉读了非常多的书,我还买了一个BOOX的墨水屏阅读器。但读的书全忘了!我甚至记不起来我读了哪些书😂,因为有的是用电子书阅读器读的,有的是纸质书。我只能大概写下我觉得还不错的书:
技术
深度学习入门
这就是ChatGPT
投资
追寻价值之路
美股70年
全球股市启示录
投资稳赚
综合
埃隆马斯克传
小而美
重来
别做正常的傻瓜
薄世宁医学通识课
技术方面的书在有了ChatGPT后,大多可以不读了,所以今年我只读了两本和AI相关的书,这两个都不错,推荐阅读。技术书贬值有多快,去多抓鱼看看就知道了。
投资方面的书我喜欢看一些和历史相关的,燕翔的三本股市回忆录的书都还不错,但看起来可能略显枯燥,因为如果你没有经历过那种行情,可能很难代入进去。指数基金之父的投资稳赚也是一本不错的书,在投资方面指数基金显然对普通投资者来说是一条且唯一一条正确的大道,这也是我的投资助手里的投资组合为什么都是基于指数基金的。
综合方面,马斯克自传的书着实给我了小小的震撼,感觉老马就是人类之光。所以我计划长期持有一些特斯拉的股票(不作为投资建议,只是个人偏好,虽然我大多在交易ETF)。小而美这本书对个人开发者来讲是挺不错的一本书,重来这本书也还不错,但感觉不太适合我。在AI的加持下,我相信未来会涌现出更多超级个体开发者,所以这本书对我来说是有一定的启发。别做正常的傻瓜这本书好像是张一鸣推荐阅读的书,看完感觉像是做了一次思维的超级训练。薄世宁医学通识课这本书不错,希望更多人能看到,因为很多人的医学常识都是完全错误的。
娱乐
娱乐方面,2023年没什么好电影,唯一不错的感觉就是诺兰的《奥本海默》,一定要选择去杜比影院看。
音乐
音乐方面,2023年给我的惊喜是刀郎的大作:《山歌寥哉》,这可能是我听的最多的一张专辑了,我还买了它的CD版本。
专辑里我最喜欢的是《序曲》、《珠儿》和《未来的底片》。从我个人角度而言,仅凭这张专辑,刀郎就已经是中国最伟大的音乐家之一了。
游戏
今年在同事的推荐下,我买了Switch与PS5,从此开始了我的游戏之路。从2023年11月底买的,两个月我已经打了快200小时游戏了,其中:
《塞尔达传说:旷野之息》:这是我玩的第一款Switch游戏,我玩了超过70小时,这是我玩过的最好的游戏,我终于理解塞尔达是天这句话了。
《塞尔达传说:王国之泪》:现在已经快20小时了,这游戏感觉还可以肝上百个小时。
《荒野大镖客:救赎2》:第一部我是在Switch上过的剧情,第二部准备在PS5上好好玩玩。
《GT7》:这个我玩了十几个小时,算是PS5上赛车类的天花板了。
《上古卷轴5》:在PS5上玩了十几小时,除了画面有点老,整个游戏还是非常好玩的,今年我会继续玩下去。
在买游戏主机之前,我也想过可能玩不进去,没时间玩,最后发现其实不过是把我刷短视频的时间转移到了游戏里,而且游戏实际上能提供更强的体验感,这甚至是电影都给予不了的。
另外一个有意思的地方在于,目前RPG游戏里有大量的NPC,大部分NPC都只会机械的重复说一些模版类的话,如果能用大语言模型来生成这些NPC的对话,那么游戏的体验会更加丰富,如果能用AI重塑这些游戏,那一定是非常值得期待的。世界也许会变得更糟糕,希望在游戏里,能给人类带来一些美好。
2024年计划
终于写完了2023年的总结,在2024年我希望我的家人都能健康快乐,我自己能有更多的时间去做一些自己喜欢的事情,比如写作、开发产品、投资等。我希望能在AI领域有更多的输出,希望能在出海方面有更多的尝试,希望能在影响力上有更多的输出,希望能在个人产品上有更多的突破。
以前我很喜欢做各种计划,但现在我希望能简单一些,能做自己喜欢做的事情就是一个不错的计划。
2024年2月,写于我的书房
更新时间: 16个月前 版本: d8cb60f29
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我的2022
我的2021
我的2020
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我的2022
27 December 2022 5:44 | 1264 字 | 8356 次阅读
年度总结
年度总结
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
2022年的这个时候,历经一周,我还没有完全走出新冠阳性的影响中,身体乏力,鼻子没有嗅觉,甚至一只耳朵的听力也减弱了很多。不过2022年竟然要过去了,如果能用一句话总结这一年的话,我会说:这是难熬的一年。
一个人的命运啊,当然要靠自我奋斗,但是也要考虑到历史的进程。说出这句话的长者在今年去世了。同样去世的人有前苏联领导人戈尔巴乔夫、英女王伊丽莎白二世与日本前首相安倍晋三。2022年从俄乌战争的痛苦揭幕开始,再到世界杯的狂欢收场,随之而走的还有国内持续三年的疫情防控。这一年发生了太多事,世界在悼念中继续前行。
作为一个普通人,除了守好自己的一亩三分地外,其他能做的并不多。虽然也想改变世界,但最终改变的只有自己。作为一篇个人年度总结,而非社会群像总结,本文力求以流水账的形式记录自己毫无意义的一年,如果读者觉得乏味,那不是你的问题。
OKR
年初我共定下了如下的小目标:
减肥5斤;
打造一款开源产品;
被动收入投资组合年复合收益率超15%;
在博客技术领域输出超过10篇文章或Session;
减肥5斤。这个目标并没有实现。我的体重不但没有降低,相比年初甚至还增加了几斤。为了实现这个目标,我今年四月到九月都是骑车上班,大概骑了几百公里。但在吃方面并没有多加控制。甚至在年底的肠胃镜检查中发现了一个增生性的大肠息肉,这和日常的饮食习惯有关,原因可能是吃的红肉过多了。为了未来的可持续发展,还是要继续减肥才行。😭
打造一款开源产品。今年在开源项目上投入了不少时间,主要精力投入到了两个项目上:free4chat与Invest Alchemy 。从Star的数量看,这两个项目都受到了一定的关注:
这两个产品目前也有一定量的用户:
被动收入投资组合年复合收益率超15%。组合的净值从年初的1.538降低到了目前的1.421(下跌7.6%),年复合收益率为8.3%。这个表现看起来很差劲,但相对其他市场的指数来说,排名从第7名提高到了第4名,说明目前这个组合的风险控制做的还不错。
在博客技术领域输出超过10篇文章或Session。今年写作了20篇文章,做了5次直播演讲。博客的年访问量近50万次:
2023年的计划
看起来年初立的小目标完成度近80%,对于这个成绩我是满意的。但2022年并没有做大量的阅读,大多数时间在刷短视频,希望新的一年能增加文字类的阅读量(尤其是社科历史类的书),也再给自己立一些小目标:
拥有一个健康的身体
肠胃健康,摒弃不良饮食习惯,减少吃红肉的频率
不熬夜,晚上11点前睡觉
夏天秋天尽可能骑车上班
提升英语运用能力
雅思能力达6666
拥有一定的个人技术影响力
Twitter关注人数破3万
博客年访问量超50万
被动收入投资组合年复合收益率超15%
通过ETF组合投资全球资本市场,降低国内权益资产占比
新一年的目标增加了对英语的提升。语言是高科技杠杆,每当我们熟练掌握一门语言的时候,也就打开了一扇新的大门。对英语的投入在此时此刻更应该加大力度。
最后给大家推荐一个视频:再读西游系列,最近我一直在看,的确让我重新认识到了《西游记》的魅力,作者的大局把控能力非常的厉害。
更新时间: 24个月前 版本: 53254d7f6
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我的2023
我的2021
我的2020
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
对交易的思考
16 May 2024 15:51 | 3488 字 | 5461 次阅读
交易系统 • 投资组合 • 交易策略 • 资金策略
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
转眼间,我的投资交易生涯已有十年之久,这十年里,从投资小白,到现在还没被市场踢出去的玩家,我对交易的理解也在不断深化。这篇文章是我在设计策引产品的过程中,对交易系统的思考,也是对我自己交易历程的总结。我会从交易策略、资金策略、投资组合与交易系统等方面展开讨论。
交易策略:假设的智慧
想象一下,交易策略其实是一种基于各种假设的决策过程。这些假设可以是任何事情,比如你可能会想,如果明天下雨,我就买入某股,如果后天天晴,我再卖出。当然,这只是一个简单的例子。真正有效的交易策略一种是基于历史数据和市场规律来建立的假设,比如技术分析中一些指标的应用。另外一些是通过研究市场机制的假设,比如ETF场内场外折溢价的套利。
好的策略就像是精心计算过的投注。它不是无的放矢,而是通过分析过去的表现来预测未来的机会。策略会在看似合适的时刻告诉你:“嘿,现在或许是个不错的投注时机。” 但重要的是要明白,任何策略都不能保证你每次都能成功。然而,如果策略设计得当,经过足够多次的“投注”,通常你会看到正向的收益。这也是为什么一个精心模拟的组合能够在长期里超越全球主流指数的原因。这些基于数据和分析的假设,使得策略不仅是理论上的设想,而是有潜力在实际市场中实现预期目标的实用工具。
资金策略:不把所有蛋放在一个篮子里
资金策略在投资管理中扮演着至关重要的角色,它决定你每次想要冒多大风险。它可以从非常简单的百分比规则发展到极其复杂的系统设计。一个好的资金策略不仅要考虑如何进场,更要精心设计如何退出,确保风险和回报之间达到最佳平衡。
交易策略告诉你这边有一个好的下注机会,而资金策略决定在这次投注中要下多少注,这样即使输了几手,你也不会一下子失去所有的筹码。这就是为什么好的资金管理对投资来说至关重要。
分散虽然是资金策略控制亏损的手段,但过度分散也是一种更隐蔽的风险。比如,趋势交易策略的胜率都比较低但盈亏比高,当组合的资金策略过于分散,如持仓超过10个标的,因为趋势策略的胜率一般都低于50%,这意味着大部分交易都是亏损的,而当少数交易盈利时,却又因为过度分散导致收益不高,最终拖垮整体组合收益。
复杂的资金策略
以一个复杂的资金管理策略为例:假设你规定单个投资标的的买入不得超过总资金的20%,以控制单一投资带来的风险。但如果这个标的表现非常好,其市值增长到超过了总资金的30%,这时你就需要通过卖出部分股份来降低其在总投资组合中的占比,这种操作是一种自动的止盈策略,帮助投资者锁定收益,同时防止单一资产因过度增长而对整体投资组合风险的扩大。
此外,资金策略还可以包括如何应对大幅下跌。例如,如果设定的止损点为投资组合总市值的2%*30%,即0.6%,这意味着在市场不利的情况下,亏损达到0.6%时,投资者将停止所有新的买入操作,这相当于是一种月度熔断机制,旨在防止在不利的市场条件下过度损耗资金。
策略的一体两面
在讨论交易策略和资金策略时,我们常常会感到困惑:它们看似都能生成买卖信号,都包含止损止盈,那它们之间有何不同呢?
实际上,虽然这两者都是策略,它们的侧重点和出发点是不同的。交易策略更多地从进攻角度出发,寻求市场机会来实现盈利。而资金策略,则是从防守的角度,关注如何管理和控制风险,确保投资组合的安全。
你当然可以选择只使用其中一种策略进行交易,但结合使用它们,就像同时装备了盾和枪,既能攻击又能防御,使你在市场中更加灵活和稳健。
在策略的具体执行上,资金策略可能会更简单一些,比如它可以只根据资金的盈亏比来做买卖决策。而交易策略则往往基于对市场的具体假设,如某种市场行为后买入或卖出。这些假设应当基于数据和概率分析,而不是随意猜测。
事实上,每次你在市场上买入时,你都在做出某种假设——假设市场会向有利于你的方向移动。无论是基于直觉还是复杂的数据分析,这些都是投资决策的一部分。因此,一个好的策略是在充分的回测和市场研究基础上建立的,以确保这些假设有坚实的理论和实证支持。
策略的有效性在于它的无效性
策略有没有好的效果在于两方面:一是它的有效性是个随行情动态变化的过程,但这种动态过程是有周期的,只有在有效周期的时候才会产生大的盈利,在策略的无效期,只能通过策略的止损来保护组合。二是信任,你只有相信它,才能穿越它的无效周期达到它的有效期,然后在它的有效周期获取大的盈利。
而策略的有效性恰恰在于它的无效性,正是这些无效周期让很多人并不相信它。如果一个策略所有人相信,那这个策略的有效性很快就会消失。你对策略的信任,是一种风险投入,而策略的有效性,是对你风险投入的回报。这也是我对盈亏同源的理解。
所以这也能理解为什么在策引全球投资组合中交易策略的胜率都是很低的,因为一个高胜率、高盈亏比的策略一旦被公开最终都会变成低胜率的,低胜率是策略公开还能存在的必要条件。
模拟组合:高级交易员的角色
在策引产品中,我设计了十多个全球投资组合,每个组合都有自己的交易策略和资金策略。这些组合都是模拟组合,那设计它们的意义何在呢?
你可以把模拟组合看作像你一样的交易员,当你订阅了这个组合背后交易策略的交易通知时,组合也在和你同步订阅这个交易策略的信号。只不过模拟组合多了一个资金策略,这个资金策略是通过控制单个标的的买入比例来控制风险,以及通过止盈止损来保护收益。实际上你也需要拥有自己的资金策略,这样你才能更好地控制风险,保护你的组合。
除此之外,模拟组合还可以全方位的展示目前策略的整体风险表现,比如最大回撤、夏普比率、年化收益等,这些指标可以帮助你更好地了解策略的优劣,以及在不同市场环境下的表现。同时,你也可以结合自己的风险承受能力和投资目标,选择适合自己的组合来订阅,从而更省心地投资。
交易系统:制定你的游戏规则
交易系统就像是给投资制定的一套明确的游戏规则。它帮助你决定什么时候买进股票,什么时候卖出,以及怎么管理你的钱。系统里包括两部分:一部分是交易策略,它基于市场的情况告诉你什么时候行动可能会赚钱;另一部分是资金策略,这部分更多关心的是怎么保证你不会因为一两次不好的决定就损失太多。
这个系统的好处在于,它不依赖你的直觉或当下的情绪,而是依靠一套事先设定好的规则来操作。这样可以让你的投资更加客观和理性,即使市场波动大,你也有明确的策略来指导你如何应对,从而更好地保护和增长你的资本。简单来说,交易系统就是你的投资指引,帮你在复杂的市场中找到正确的方向(这也是策引名字的由来)。
长期投资:耐心是王道
在投资世界中,耐心是非常重要的。就像在投注中不能期望一夜之间赢得大奖一样,有效的投资策略需要时间来展现其真正的价值。你需要有耐心,坚持下去,让概率的力量发挥作用。要让概率产生作用,长期投资必不可少,因为投资期限越长,概率越能产生作用,这样才能从众多小亏损中,捕捉少数的大的涨幅。
个性化投资体验
每个人的投资体验都是独一无二的,就像每个投注者在赌场的运气一样。你的成功取决于你如何执行策略,你的情绪控制,市场时机,以及你的资金管理技巧。这意味着你的投资表现可能与模拟组合不同,尤其是当你对策略没有信心时,更容易产生错误的决策或者无法继续跟踪策略,而这时概率并没有帮助你,反而会伤害你的实盘。
不要预测市场,跟随它
投资时,很多人喜欢尝试预测市场将会怎样,但实际上,更靠谱的策略是“跟随市场”。就像在水里游泳,你不需要知道每一波浪的来历,只需要顺着流动就好。
交易策略也是这样,它不是去猜市场的下一步会是什么,而是观察市场的动向,然后做出响应。这种方法不是试图控制市场,而是适应市场。就像是在跳舞,你要跟随音乐的节奏,而不是自己创造音乐。
这样做的好处是你不会因为错误的预测而被市场淘汰。市场总是对的,我们要做的就是学会如何与之同行,而不是试图去领导它。
控制亏损,剩下的交给市场
当我们谈论投资时,有一句话很经典,也非常贴切:“切勿让一次小亏变成大亏。”这就是止损的重要性所在。止损是你的安全网,保证你在市场不利时不会失去太多。止损也是交易策略里很重要的一个点,它可以解决交易中何时卖出(退出)的问题。
同样重要的是我们的资金策略,即怎样分配你的投资本金。简单来说,就是不要把所有的钱都压在一张牌上。适当地分配风险,可以让你在一个投资亏损时,其他可以支撑住整体。
总之,控制好你的亏损,然后让市场去决定剩下的。通过长期投资,获取概率的力量,控制亏损,你就可以在市场中取得成功。
总的来说,虽然投资和投注有一些相似之处,但投资是一个更加科学和系统的过程。通过深入了解这些投资组件,你可以构建一个更稳固、更智能的投资策略,帮助你在市场中取得成功。
更新时间: 12个月前 版本: 69d08dd7f
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我的投资助手
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我的投资之路:从迷茫亏损到系统构建与“策引”的萌芽
29 April 2025 26:55 | 5923 字 | 1700 次阅读
投资理财 • 指数基金 • 指数定投 • ETF • 股票交易 • 交易系统 • 上班族理财
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
这篇文章整理自最近参加的《捕蛇者说》的播客节目:AI能否帮我们做出更好的投资决策?。
今天想和大家深入地聊一聊我在投资这条道路上摸爬滚打、不断探索的心路历程。这不仅仅是一个关于钱的故事,更是一个关于认知迭代、技术实践以及如何与自己的人性博弈并最终萌生“策引”这个想法的旅程。
2015年的那一跤:一次刻骨铭心的“觉醒”
我的投资故事,不可避免地要从2015年的那场A股巨震讲起。和当时很多初入市场的“小韭菜”一样,我带着一股初生牛犊不怕虎的热情,凭着感觉和对某些股票的“看好”,就把相当一部分资金甚至是重仓压了进去。结果,市场并没有如我所愿,急转直下,出现了大幅回调。
当时的我,完全被情绪牵着鼻子走,脑子里没有任何止损的预案。唯一的念头就是“死拿”,坚信“它总会涨回来”。结果是残酷的,我的个人亏损最终超过了50%。
那段经历不仅仅是金钱上的损失,更是对我之前所有投资认知的彻底颠覆。我亲眼看到身边一些朋友,本金高达百万甚至千万级别,在2015年的股灾中损失惨重,甚至有人把之前赚了好几倍的收益全部吐了回去。更令人震惊的是,即便是在金融领域摸爬滚打了十几年的资深人士,甚至有像Wind这种金融软件公司的架构师,面对市场的剧烈波动和情绪的干扰,也未能幸免于难,将之前赚到的钱全部亏了回去。
这让我深刻地意识到,在投资这个“战场”里,单凭个人的感觉、所谓的“小道消息”,或者仅仅是嘴上喊着“价值投资”(特别是在对公司基本面一无所知的情况下),是多么危险和不可靠的一件事。人性的贪婪和恐惧,在市场的波动面前暴露无遗。
从书中汲取力量:寻找对抗“人性弱点”的技术解法
经历了惨痛的亏损后,我开始痛定思痛,大量地阅读投资领域的经典书籍。我希望能找到一条更理性、更有系统性的道路来指导投资,而不是依赖于情绪和不确定的消息。
其中,《海龟交易法则》和《走进我的交易室》这两本书对我的影响尤为深远。它们不约而同地强调了通过交易策略来构建个人交易系统的重要性。这就像是给我打开了一扇全新的大门——或许,技术和系统化的方法,才是帮助我克服自身人性弱点、进行更理性决策的“武器”。
我的大学专业是软件开发。虽然在国企和后来的创业期间,写代码的机会并不多,但我自学了iOS开发,希望能用技术来辅助我在投资上的实践。最初的想法是做一个交易助手App。
在那之前,我先用VBA开发了一个Excel工具。这个工具对我来说,与其说是简单的记账软件,不如说是我的“飞行日志”和“飞机仪表盘”。它的核心功能是用来记录我的每一笔交易操作细节,比如买入价、计划卖出价、止损价等等。更关键的是,它能够参照基金净值的计算方法,自动计算整个投资组合的实时净值,并展示一些重要的风险指标,例如年复合收益率、最大回撤幅度与时间等。
有了这个Excel工具,我开始逐步摆脱情绪化的交易。每次计划买入前,我都会强迫自己在系统里设定好详细的交易计划,明确止损和止盈的价格。这迫使我进行更有纪律性的思考和决策。同时,通过宏观地观察整个组合的整体表现和风险指标(就像飞行员盯着仪表盘),而不是仅仅纠结于每一笔单独交易的盈亏,我能更清晰地了解我的操作是否对整体风险指标产生了积极的影响。这个系统化的记录、执行和反思过程(也就是“交易日记”),帮助我养成了更好的交易习惯,从而提高了我的决策能力。很多投资大师也推崇这种通过交易日记来提高自身能力的方法。
性价比的拷问:散户的“主战场”在哪里?
尽管这个Excel工具在帮助我克服情绪化交易上起到了很大作用,但它的局限性也很明显:操作起来相对复杂,需要频繁地手动输入数据。而且,随着时间的推移,我逐渐意识到一个更根本的问题:对于本金不多的普通上班族散户来说,即使拥有再好的交易系统,如果本金基数太小,那么所有的努力似乎都显得微不足道。收益率再高,如果乘以一个很小的本金,赚到的钱也无法对生活产生实质性的影响。
我开始深刻反思,将大量宝贵的业余时间投入到复杂的交易系统维护和频繁的交易操作中,这种“性价比”实在是太低了。这让我意识到,对于绝大多数上班族而言,更明智的策略是把主要精力放在提升自己的主业技能、发展副业上,努力提高自己的本金收入。投资,不应该是占据生活全部的活动,它应该是一个为我们生活提供辅助和支持的工具。这也和我读过的一些投资理念不谋而合,比如不要把投资当做生活的全部,而是要提升主业收入,享受生活,因为如果把投资当做生活的全部,可能会活得很悲惨。
那么问题来了,对于像我这样没有太多本金和时间,但又希望能系统化参与投资的上班族来说,有没有一种更“傻瓜式”、更省心的方法呢?我尝试了一些市面上的现有选择。
首先是公募基金。然而,我很快发现公募基金经理的利益诉求和我们普通投资者并不总是完全一致。他们可能更关心管理费的规模和新基金的发行,而不是能否长期持续地为投资者赚取可观的收益。市场好的时候疯狂发新基金,吸引散户资金,这种模式显得不够透明,也不完全是以投资者长期利益为先。
其次是关注网络上的各种“大V”。他们通常会展示各种光鲜亮丽的组合业绩,但这背后的具体方法往往是一个“黑箱”,缺乏透明度。更重要的是,我长期观察下来发现,绝大部分网络大V最终似乎都“翻车”了,这好像只是一个时间问题。一旦成为大V,他们的利益结构也可能发生变化,他们可能会依赖流量甚至通过虚假的业绩来割韭菜。这种不透明且风险极高的方式,我个人无法接受。
返璞归真:构建透明、简单的策略系统
排除了这些选项后,我回到了原点——我需要一个更透明、更简单、更容易跟随的投资方法。一个即使本金不多,也能让我安心把大部分精力放在提升主业上的方法。最终,我决定回归技术本身,自己动手构建一个程序化的交易策略系统。
可能会有人质疑,程序化策略在像美股这样高效的市场中,理论上因为信息会瞬间被消化,可能已经失效了。但市场是极其复杂的。而且,A股市场与美股市场存在很大不同,A股市场的散户占比更高,相对来说更接近一个非有效市场。在这样的市场环境中,一些经过充分回测验证的简单策略,反而可能持续有效。
大约在2018或2019年,我开始实践并使用最简单的双均线策略。这个策略的核心原理就是跟踪趋势。它简单到许多人可能觉得它不可能有效,但经过我反复的回测和实践,我发现它在A股指数类标的上表现是可行的,甚至收益率在指数不涨的十年里(如上证指数在1000点到3000点之间波动)也能达到年化7-10%以上。这恰恰满足了我对一个制度化、透明、可供机械跟随策略的需求。
在选择交易标的时,我选择了指数(如ETF)而不是个股。这是因为在A股市场,个股在极端行情下可能遭遇流动性枯竭甚至停牌,即使我的策略发出了明确的卖出信号,也可能无法成交,导致策略失效。而像大盘指数这样的流动性极好的标的,无论何时都可以自由买卖,确保策略发出的指令能够被有效地执行。我认为,一个好的交易策略,必须在流动性充足的标的上才能真正发挥作用并被有效地执行。
“策引”的诞生:从简陋脚本到AI探索的自动化之路
有了这个简单双均线策略并在实践中验证了其有效性后,我开始思考如何将其自动化,从而减少手动操作的繁琐。最初,我只是在AWS上部署了一个简单的Python脚本。这个脚本每天定时运行,计算策略信号,并将交易记录上传,最原始的方式是通过邮件列表将交易信号发送给订阅者。功能非常简陋,但出乎意料的是,居然有人对此表示了兴趣并进行了订阅。
随着订阅人数的增加,我开始认真思考将这个简单的策略服务进行产品化。正好那时,像GPT这样的大模型横空出世,极大地提高了我的编程效率。我也恰好有一些将开源项目产品化的经验(比如之前的myGPTReader项目)。这些因素叠加,让我意识到可以将这个双均线策略的服务也做成一个更正式的产品。这就是“策引”最初的雏形。“策引”这个名字,最初的含义是“我的投资助手”。
最初的“策引”功能非常基础,本质上只是一个均线交易提醒产品。付费用户很少,大家普遍觉得功能过于简单,不值得付费。这让我意识到,产品需要不断地迭代和增加更实用的功能。
在产品迭代过程中,我尝试将AI能力融入进来。我开发了一个面向海外用户的英文AI助手,叫做“Chat to Invest (Chat2Invest)”。它的功能是帮助用户通过对话的方式分析股票或指数的情况。比如用户输入一个问题或想分析的标的,AI会先提取股票符号,然后后端程序获取相关数据和技术指标生成图表,再将这些信息反馈给AI进行分析和解读。
虽然这个AI助手的功能目前正在重构和设计中,因为它之前的版本确实比较粗糙。但这次尝试让我看到了AI在投资辅助方面的巨大潜力。同时,我也更加认识到回测功能的重要性。回测能够揭示一个交易策略背后真实的风险指标,比如波动率和最大回撤。这也是为什么“策引”的模拟组合中要详细展示这些指标和历年表现的原因。我的目的是让用户充分了解策略的风险特征,而不仅仅是看到一条“漂亮”的收益率曲线。我相信,只有真正理解了这些风险指标,结合自己的性格特点和风险偏好,投资者才能判断一个策略是否真的适合自己,并有足够的信心去长期、有纪律地跟随它。
“策引”的未来展望:一个AI赋能的个性化投资伙伴
我对“策引”未来的发展方向充满期待。虽然这是一个业余时间的作品,但我希望将AI助手的功能做得更加强大和智能化,使其不仅仅是提供信息,更能帮助用户理解、构建、回测、优化甚至推荐那些最适合他们个人情况的交易策略。
这需要“策引”底层的策略系统变得更加灵活。我希望它能够通过配置化的方式,动态地生成各种复杂的交易和资金管理策略,而不仅仅是写死的代码。AI可以利用“策引”强大的回测能力,不断地尝试和优化这些策略。这听起来可能非常有野心,有点像在做量化机构的事情。
但我的想法是,量化机构赚的是量化的钱,普通散户赚的是普通散户的钱,两者之间并不矛盾,甚至可以互补。对我而言,“策引”首先是作为满足我个人需求的业余产品。我自己在真实地使用它,并根据我自己的需求不断地打磨和改进。这种“先满足自己,再寻找同类用户”的开发模式,与传统的市场调研驱动的产品开发方式有所不同。“策引”的高度透明度以及对详细指标的展示,正是我作为使用者最渴望看到的,我也希望它能为其他同样追求透明、希望深入理解策略的投资者提供真正的价值。
“策引”的核心目标,是希望降低使用专业工具和执行交易系统的门槛。希望通过AI的赋能,让这一切变得更简单易用。它旨在帮助用户更好地执行既定的策略,将更多的精力专注于提升本金的主业上,而不是被频繁的盯盘和交易消耗掉宝贵的时间和精力。
人性是最大的挑战,AI的角色是什么?
然而,我也深知,即使拥有了再完善的系统和工具,人性的情绪化依然是投资道路上最难逾越的障碍。一个好的系统或策略可以冷酷地根据预设的规则发出买卖信号,完全不理会市场上的各种消息和由此产生的情绪波动。比如之前特朗普关税大战的消息出来时,市场情绪波动很大,但“策引”中一些使用趋势或止损策略的美股相关组合(如QQQ组合在2月12日),可能仅仅因为价格趋势破位,就已经根据策略信号进行了空仓操作(而且这并不是过拟合的回测)。这完全是基于客观价格信号的反应,而普通投资者在面对这类突发新闻时,往往容易被市场的恐慌或侥幸心理影响,难以做到如此果断的纪律性执行。但作为有情感的人,我们却很难完全做到这样冷血的执行。即使看到了系统的信号,我们也可能因为恐惧错失机会、因为贪婪而犹豫不决,或者抱有“总会涨回来”的希望,导致无法完全、及时地执行系统的指令。
而且,很多趋势策略的胜率可能并不高,这意味着你会频繁地遇到小额亏损。普通人很难承受这种持续的心理压力,容易在中途因为亏损而放弃,最终无法坚持到策略成功捕捉到大的市场趋势、实现可观盈利的时候。
这正是我希望AI能够帮助解决的问题之一。除了策略的生成和回测辅助,我希望未来的AI助手能够拥有长期记忆功能。在与用户的长期交流和互动中,它能够逐步了解用户的真实风险偏好、性格特点以及在交易中容易暴露的弱点。它就像一个耐心、客观、不带情感偏见的朋友或教练。它可以分析你的想法、你的交易记录,帮助你认识到自己在交易中的性格缺陷,比如冲动、犹豫不决、喜欢听消息等等。就像我当年通过写交易日记来反思和认识自己一样,AI可以成为一个强大的辅助工具,帮助你更好地认识自己,并适时地提醒你坚持那些符合自己风险偏好的策略。它不会一味地顺着你,而是会基于客观数据和概率,提供客观甚至有时是“逆耳”的观点,促使你更深入地思考自己的决策。
这种通过对话促进自我认知的作用,我近期就有着非常深的体会。 前段时间,在做一个风险极高的决策时,我花了一周的时间,与多个当前顶尖的大模型(像是Gemini、Claude等)进行了深入的、反复的讨论。我把我的思考逻辑、市场判断、风险顾虑,以及搜集到的最新信息,都尽可能坦诚地告诉它们。有趣的是,不同的AI会有不同的‘性格’和反馈,有的谨慎保守,有的则在充分信息下能理解甚至认同高风险策略。这个过程的关键并非让AI替我做决定,而是通过向它们解释、辩论、提供证据,我被迫一次又一次地审视和澄清了自己的真实想法、风险承受能力以及决策依据。虽然AI的观点有时客观到逆耳难听,但这种反复的思维碰撞,确实极大地帮助我深化了对自身决策和市场情况的理解。这让我更加确信,一个具备长期记忆、能进行深度对话的AI助手,在帮助投资者认识自我、坚持原则方面,潜力巨大。
但我并不打算让AI直接进行自动交易操作。首先,这涉及到非常复杂的合规和法律风险问题。其次,对于“策引”目前采用的这些低频交易策略(比如很多美股组合可能几年才交易几十次,频率非常低),自动化执行的必要性并不大。最关键的一点是,我希望最终的投资决策者仍然是用户自己。只有自己对交易负责,才能真正从每一次操作中学习和成长。工具和AI可以提供信息、分析、洞察和辅助,但最终的决策者和由此产生的后果承担者,是你自己。这就像古代的谋士可以提出各种各样的策略和决策建议,但最终选择采纳并执行哪个的,是做出决策的人。AI无法替代你自身的成长。
结语
总而言之,我的投资之路是一条不断探索、反思,并将技术能力应用于实践的道路。从最初简陋的Excel工具,到曾经尝试的交易日记App,再到现在的“策引”平台,以及未来希望深入融入的AI功能,这一切的努力都是希望能找到一种更适合普通人、更理性、更易于执行的投资方式。
它不是为了让你抱着一夜暴富的幻想,而是希望通过系统化的方法,在风险可控的前提下,帮助你实现资产的稳健增长。更重要的是,它能让你将更多的宝贵精力放在提升自己的主业收入、享受生活上,而不是被投资的焦虑和复杂性所困扰。
这就是我的故事以及一些还不成熟的想法。希望我的这些经历和思考,能为大家在投资这条路上带来一些小小的启发。
更新时间: 1个月前 版本: 08ac1662a
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
2022亏了多少组合季报(2022Q4)
我的巨亏经历组合季报(2022Q3)
交易之难组合季报(2022Q2)
疫情与战争组合季报(2022Q1)
复盘2021 组合季报(2021Q4)
三周年小记组合月报(202109)
仓位管理是核心组合月报(202108)
护城河还在但城没了组合月报(202107)
最牛指数组合月报(202106)
慢慢变富组合月报(202105)
家庭资产配置的阶段组合月报(202104)
长期投资之难组合月报(202103)
重新思考估值策略组合月报(202102)
当别人的股票基金上涨时组合月报(202101)
投资的秘密组合月报(202012)
关于银行分期贷款的坑组合月报(202011)
投资交易的心理建设组合月报(202010)
大跌时我们能做什么组合月报(202009)
双均线交易策略组合月报(202008)
投资理财书籍推荐组合月报(202007)
200万小目标组合月报(202006)
2019年金融投资总结
小白的交易之路
年轻上班族的系统化投资之路
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
构建自己的信息简报
24 November 2022 4:45 | 1045 字 | 9203 次阅读
Email • RSS • 信息输入 • 如何阅读
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
我喜欢阅读,除了看书这种传统的获取知识的途径外,获取信息的渠道或方式还有:公众号、邮件列表、社交媒体、垂直社区(如知乎、V2EX)、搜索与聊天群组。如此多途径,怎么管理?
信息与知识的差异在于,信息必须经过特定的处理,才能转化成人能理解与应用的知识。
仔细看这些渠道和方式,可以分为三个大的类别:
信息展示:大量的信息以这种方式去公开发表,需要人们通过主动或被动的方式去获取;
主动方式:如社交媒体与垂直社区,就需要我们主动刷新的方式获取最新的信息;
被动方式:如公众号与邮件列表,需要我们去订阅后被动接受更新后的信息;
搜索:带有目的性的,通过特定关键词去获取与关键词相关的公开信息集合的方式;
沟通交流:主动获取信息的方式,可以获取到非公开发布的信息;
本文主要研究如何高效处理第一种获取信息的方式:信息展示。信息展示的主动与被动方式可以通过特定的途径转化。比如可以通过自动化的手段把主动方式变为被动方式,典型的如我们可以利用古老的RSS来订阅社交媒体与垂直社区的信息。当然也可以主动去阅读公众号的历史文章,去查看邮件列表背后网站的历史信息。
主动刷信息是非常低效的行为,所以我希望有一种方式能实现以下的需求:
能被动的推送信息;
能方便的查看历史归档信息;
能将不同来源的信息定期汇总阅读;
能订阅全球的信息;
能利用常用的工具自动化的实现此需求;
微信公众号只能实现前两条,社区类网站也无法实现第三和四条。上文曾提到的RSS可能会满足我们的要求,很多古老的网站都提供RSS订阅信息的更新,但社交媒体或Feed类的网站一般都不支持RSS。经过一番搜索后,找到了这个工具:RSSHub,可以通过RSS订阅一切网站!
有了这个神器后,我们就可以实现上述的除三之前的所有需求。如何才能实现用RSS把不同来源的信息组合到一个页面中?经过一番搜索,我又找到了Mailbrew这个网站,它可以把多个来源的信息组装到一个页面,定期投送该页面到邮箱,这就实现了我们所有的需求了,仅仅只需要一个邮箱来订阅与阅读!
用邮箱来阅读有以下好处:不增加工具复杂度、能方便的检索历史信息、能被动接受信息、能设定自动化的规则去分类信息(如Gmail可以通过设置Filter规则来自动分类邮件)。所以最终解决方案就是:RSS + Email。
我用Mailbrew制作了以下两个邮件列表:
Dev Weekly
News Weekly
Mailbrew每周会自动给我汇总一份信息简报投递至邮箱,我不需要主动去很多分散的网站来获取信息,只需要定期查看邮件即可。
如果你有更好的解决方案,欢迎留言交流。
更新时间: 30个月前 版本: 4bdd46aaf
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我的AI阅读助手
构建高质量的信息输入渠道
构建终身学习体系进行自我提升
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
财富常识
2 January 2022 33:23 | 7346 字 | 26483 次阅读
家庭财务 • 财务自由 • 投资理财
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
追求财富,而不是金钱或地位
市场经济是人类与生俱来的属性
不靠运气赚钱
打工无法变富
互联网带来的机会
商业杠杆
人力和资本是古老的杠杆
产品和媒体是新的杠杆
选择带杠杆的商业模式
获得独特知识
打造个人品牌
过简单的生活
慢慢变富
我出生于一个贫穷的农村家庭,很长时间我的家庭都面临着财务困难。所以在大学毕业刚工作时,我就因为穷而被迫研究一些投资的知识。一开始是学习一些基金的知识,然后尝试股票交易,最后甚至考了证券从业资格证。在交了不少「学费」之后,我对投资理财,甚至家庭财务规划都有了一定的认知。
在多年的折腾中,有个问题始终在我的脑海中,那就是:财富究竟是什么?
上学时我对财富的认知就是有钱,有很多钱。之所以这么想也很简单,因为在上学时家里穷,以至于只能通过各种节省钱的方式来生活。父母甚至会为节省几块钱坐车而在寒风中等几十分钟。这些经历无时不在告诉我,财富就是有很多足够生活的钱。我的父母也从来没有告诉我如何获得财富,他们天然的以为这一切等我工作以后就有了答案。
工作之初加入了一家国企,这个阶段我对财富的认知是,财富就是好好工作,赚钱买房成家。之后我换了很多公司与岗位,期间经历了自己创业,眼看公司从估值高点然后快速破灭。失业后在家蹲了半年学习技术,之后又去了私企,最后到外企打工。这个阶段中途我对财富的看法是,财富就是能达到财务自由,详细的说就是躺着通过投资的方式赚的钱能覆盖生活开支。当然这种自由是有局限性的,开支越高对投资收益的要求就越高,反之的话会简单一些。
当然我也看了很多关于财富的书,比如大多书里提到财富时会说资产与负债。以我们的房子与车子来说,房子就是资产,车子就是负债。当然这是对财富最简单的模型刻画。房子如果是投资性的话就是资产,如果是自住的话并不算资产,实际上自住房产每年也有折旧。车子是负债是因为车子是自用的,有很高的折旧与使用成本,无法产生投资收益。
资产与负债的另一端是收入与支出,收入与支出差就产生了现金流。现金流可以让我们购买生息资产,生息资产可以产生被动收入,如果被动收入大于我们的生活开支,那我们就财务自由了。如果你想体验财务自由的话,可以玩玩这个大名鼎鼎的 现金流游戏。
听起来一气呵成,好像很简单。但现实世界并不是这么简单。比如以投资来说,股市里就有无数可以让你亏损的方法。创业也有无数你想象不来的死法。这里面最大的风险就是认知带来的。通过运气赚来的钱,通过实力亏没。关于财富与认知,可以看我之前这篇 慢慢变富 里讲述了我上学时所在县城不少人暴富返贫的故事。
这就是我对财富一些粗浅的认知。当然前面这些只是本文的引子,以下是正文开始。
这两天刚好在Twitter看到一个 How to Get Rich (without getting lucky) 帖子,刷新了我对财富的认知,所以花了点时间看完了这些帖子。由于原贴非常长,有人整理成了 PDF。
帖子的作者 Naval 是一个投资了上百家科技公司(包括一些「独角兽」)的天使投资人,他自身也是持续创业者。
接下来我会按照他帖子的一部分结合自己的理解(不少是我的个人理解,并不是对原文的直译) 来讲述他对财富的认知。由于原贴的主题众多,不限于财富,建议有时间可以看一遍原帖。
追求财富,而不是金钱或地位
Seek Wealth, Not Money or Status
这点很好理解。财富并不和金钱完全等价,它是一种可以在你躺着也能增长的资产。从这个角度看财富具备被动收入的属性,如果是金钱的话,无时无刻不受到通货膨胀(CPI)的影响。90年代的万元户放在现在啥也不是,就是因为通货膨胀的不断增长导致。但如果你在90年代开始做投资,比如房地产或科技公司的话,现在这笔钱可能已经是一笔巨款了。所以我们可以通过投资来让金钱变成资产。
那为什么不应该追求地位呢?「人是一种追求相对位置的猴子」。相对位置就是地位,地位是一种零和游戏。举个例子就是在一个班级里,小明考试得了99分,但他一点也不高兴,因为他排名第二,第一名得了100分。在零和游戏里,第一才是王道。
地位是一种古老的游戏,早在人类还是猴子的时候就已经开始玩这个游戏了。有意思的是,玩地位游戏的猴子喜欢攻击玩赚钱游戏的猴子。想想在古代帝王统治的时代,「万般皆下品,唯有读书高」,这里读书人玩的就是地位的游戏。「士农工商」,最底层不是农民劳工,而是商人,商人玩的就是赚钱的游戏。
尽可能避免零和游戏,因为它并不创造财富。另外一点在于现在的世界由于生产力的大幅度提升,多元化的选择出现了,我们可以通过创造财富而不是通过攀爬高位而获得财富。
市场经济是人类与生俱来的属性
Free Markets Are Intrinsic to Humans
我们的父辈经历过计划经济,之后改革开放,市场经济出现了。可能到现在我们已经习以为常了能自由交换的市场。市场经济也叫自由经济,换个说法可能更知名:「资本主义」。
作为一个社会主义接班人,我们所学的马列主义,总会提到资本主义的缺点。到现在很多自媒体嘴里,动不动就会提到资本的原罪,无脑的媒体(或是掌握了流量密码)总会动不动把很复杂的经济问题归结为资本。「资本和资本主义是两个并不同的概念」。
We use credits and debits to cooperate across genetic boundaries
人类是动物界中唯一能跨越基因结界而产生合作的动物。大多数动物不会产生跨种族的合作。但我(Naval)可以和你们合作。你们中的一个是塞尔维亚人。另一个另一个是波斯人的后代。而我(Naval)是印度裔。我们很少有共同的血缘关系。但我们仍然合作,是什么让我们合作?是因为我们可以记录下借贷的情况。谁投入了了多少工作?谁贡献了多少?这就是自由市场资本主义的全部。因此,我(Naval)坚信,这是人类与生俱来的,我们将创造出越来越多的财富,让每个人都有富足的生活。
关于这一点,我强烈推荐看下《原则》一书作者 Ray Dalio 的 经济机器是怎样运行的。
不靠运气赚钱
Making Money Isn’t About Luck
相信你也有过这种「通过运气赚来的钱,通过实力亏没」的经历,如果你没有,相信周围人也有会这样的例子。通过运气赚来的钱,典型的就是买彩票了。已经有一些新闻报道过中彩票大奖的人最后不止返贫还欠了一屁股债。也有不少拆迁户拿着巨款被人下套骗走钱。
运气也分类别,以下是四种运气:
狗屎运(Blind luck)。在股票市场里投资(或赌场赌博)很容易发生这种情况,市场先让你赚一笔钱,并让你以为是自己靠实力来赚的。当你上头后,又加码投入了大量的钱,最后你破产了。
折腾而来的好运(Luck from hustling)。比如我通过写文章,有人看到了我的文章而产生了与我合作的机会。我通过折腾而获得了一些机会,赚到了钱。这种就是折腾而来的好运。
提前准备而来的好运( Luck from preparation)。「机会不给无准备之人」。当闪电来临的时候,你要在场。
独一无二的好运(Luck from your unique character)。这是最难的一种好运。它需要你具备独特的性格、独特的品牌、独特的思维模式。比如你是某个领域里的专业玩家,可以做一些别人做不到的事情,并且别人也知道你有这个能力,当他们需要你帮忙的时候,他们就把他们的好运转给你了,你通过这种独一无二的好运而赚到了钱。往大了说,创业的好运就是这样,你具备别人不具备的优势,发现了市场的商机,通过打造特定的商业模式解决了问题,你也得到了财富。
相信你看了也会觉得后三者的好运其实并不是完全的运气,而是由你的努力而产生的。而第四类好运,就是我们尽可能追求的好运,我们的学习与我们的技能,无一不为此而准备。当这一类好运来的时候,我们抓到了,我们也就获得了属于自己的财富。
打工无法变富
You Won’t Get Rich Renting Out Your Time
打工就是靠出卖你的时间来获得现金。这个赚钱模式有很多问题:
时间是有限的。就算如律师和医生一样很高的时薪水,由于没有杠杆,你赚到的钱和时间是线性关系;
主动收入。无法躺着,一旦停止打工,收入也就断了;
无法积累资产。你所有的产出(知识产权)都是公司的,就算打工100年,你的积累都是别人的,你是在为别人而赚钱;
对平台的依赖。你的能力很大部分是平台给予的,脱离平台啥也不是。脱离公司,能赚到钱很难;
Renting out your time means you’re essentially replaceable
由于高度依赖某类公司,让你变的可替代。公司也会想方设法通过各种手段(老带新、培训、第二梯队)让你变的可替代。最近很热门的「内卷」、「996」、「35岁失业」都是由此而产生的。
You must own equity to gain your financial freedom
大多人都是生而为打工人,要跳脱这个死循环很难。但也可以从构建自己的资产开始,通过投资的方式获得创造被动财富的机会。
You want a career where your inputs don’t match your outputs
请选择那些付出与收获严重不匹配的职业。如果你是一个木工,哪怕是最优秀的木工,你也无法赚更多的钱。但如果你是一个优秀的设计师或工程师,你可以通过工具创造杠杆来赚取远超你投入成本的钱。「技术本质是一种高效的时间杠杆」,我们通过打造技术优势,而获得「时间杠杆」,让打工中的线性关系变成指数级。互联网本质就是这样的生意模式,技术让互联网行业的参与者赚到了指数级的钱。
这也是我一直追求的模式:学习(认知杠杆)*技术(时间杠杆)*投资(财务杠杆) =>被动收入(人生杠杆)。
互联网带来的机会
The Internet has massively broadened the possible space of careers
互联网催生了不少新的职业,比如现在很火的自媒体,还有电子营销等。这些新的职业尤其是和内容营销相关的行业,都很容易创造被动收入。比如你是一个YouTube创作者,几年前创作的视频都在源源不断的为你赚钱。你创造的内容就是你的资产。还有你现在看到的文章,也是我创造的资产,它可能为我带来新的机会进而为我赚钱。
The Internet allows you to scale any niche obsession
互联网的另一大特点就是极低的传播成本,互联网打破了信息传递的壁垒。你可以把细分领域里的生意(Niche)通过一定的传播手段分发给世界各种的用户,获取更大的收益。极低的传播成本意味着我们可以很容易建立与他人的连接,连接引发新的合作机会,这不就是折腾而来的好运(Luck from hustling)吗?
商业杠杆
人力和资本是古老的杠杆
财富需要杠杆。
财富杠杆是什么?我们知道「商业杠杆来自于没有边际复制成本的资本、人力和产品」。所以每个人都在抢夺人力和资本。但显然我们的大脑还没有进化到可以理解新的杠杆形式的程度。
Labor and Capital Are Old Leverage
最古老的杠杆是人力,也就是雇佣劳工为你工作。老板可以雇佣很多劳动力为他工作。但社会高估了人力作为一种商业杠杆的价值。这就是为什么你父母在你升职时经常会问你管多少人。人们会用管多少人来评估你的职位。他们会用这个作为建立可信度的一种方式。他们试图衡量你有多少人力杠杆和你到底有多大的职权影响力。或者当有人发起一个运动时,他们会说他们有多少人。我们只是下意识的认为人越多越好。但管理人是非常难的,它需要巨大的领导技能。你离叛变、被吃掉或被暴徒撕碎只有一步之遥。这是个令人难以置信的竞争。整个文明已经在这场斗争中被摧毁。例如共产主义、马克思主义都是关于资本和劳工之间的斗争。
Capital has been the dominant form of leverage in the last century
接下来是资本出场。它可能是上个世纪最主要的的商业杠杆。你可以通过谁是最富有的人看到这一点。银行家、腐败国家的政客、印钞票的人,基本上都是最富有的人。在许多老牌上市公司里,CEO的工作实际上是一个金融工作,他们实际上是资产管理经理。
但资本的杠杆需要专业的技能才能驾驭。一百年前的管理技能今天可能仍然适用,但一百年前的股市投资技能在今天可能并不适用。资本是一种更棘手也更强大的杠杆使用形式。它可以被转换为劳动力,也可以转化为其他东西。在内心深处,我(Naval)认为我们都不喜欢资本作为一种杠杆形式,因为它并不公平。它是一种无形的东西,可以积累起来,可以代代相传,但突然间似乎导致人们拥有巨大的财富。
如何获取资本?
如果你在某一领域有专业的领域知识,如果你负责任,而且你在该领域有一个好名声,那么人们就会把资本作为一种杠杆形式给你。你可以用它来获得更多的资本。
资本也是相当好理解的。我(Naval)认为很多对资本主义的抨击都是因为资本的积累。
产品和媒体是新的杠杆
Create software and media that work for you while you sleep
当产品没有了边际复制成本后会怎么样?如果你有这样的想法,你可能获得了一种新的杠杆形式。这只是在过去几百年里发明的。它是由印刷术开始的,随着广播媒体的发展而加速,现在它真的随着互联网和软件的发展而爆炸了。
现在,你无需人力杠杆,也无需资本杠杆。只需要一台接入互联网的设备,就可以使用这种新的杠杆:产品和媒体。
这种最新的杠杆形式是所有新财富的来源,所有新的亿万富翁。上一代人,财富是由资本创造的。那是沃伦·巴菲特的世界。但新一代的财富都是通过代码或媒体创造的。像比尔盖茨、乔布斯、扎克伯格、贝索斯都是这个新世界的大玩家,他们的杠杆来自代码。
Combining all three forms of leverage is a magic combination
科技公司是什么?从杠杆的角度看,科技公司是由以上三种杠杆组合的。科技初创公司通过一个技术想法,吸引最优秀的人力杠杆,也就是工程师、设计师、产品开发人员。然后引入资本杠杆,把资本转化为营销、广告和全球化扩张的形式,最终把代码、媒体和内容创造的产品通过互联网传播出去。
这是一个神奇的组合,这就是为什么你看到技术初创公司突然爆发的原因,因为它使用大量的杠杆,成功后自然获得巨大的超额回报。
Product and media leverage are permissionless
产品和媒体是无许可的。你无需其他人的帮助,甚至也不需要资本的帮助,只需要一台联网的设备就可以开始创造财富的过程了。比如你可以开始编码、写书、录制播客、发推特、创作YouTube视频。你不需要任何人的许可来做这些事情,产品和媒体是非常平等的杠杆。
这就是为什么我告诉人们要学习代码。在未来,会有这些机器人,他们会做所有的事情。这可能是真的,但我想说,机器人革命的大部分已经发生了,而且机器人的数量远远超过人类。只是我们为了散热和效率的原因,把它们装在数据中心里。我们把它们放在服务器与电脑里面。所有的电路,都是机器人的头脑在里面做所有的工作。每一个伟大的软件开发人员,现在都有一支机器人大军为他工作。在晚上,当他或她睡觉的时候,在他们写完代码后,它就开始工作了。
机器人大军已经来了,机器人革命已经发生。我们大约进行到一半。我们只是添加了更多的硬件组件,这些随着我们对自动驾驶汽车越来越满意,也许还有自动驾驶卡车、飞机与轮船。有送货机器人和波士顿动力机器人等等。例如,正在为您进行网络搜索的机器人已经在这里,那些回答许多客户咨询的机器人已经在这里,你可以订购这支机器人大军。但这些机器人不是很聪明。你必须用他们能理解的语言去发出命令(编码)。
选择带杠杆的商业模式
An ideal business model has network effects, low marginal costs and scale economies
理想的商业模式具备网络效应、低边际成本与规模经济。
规模经济:生产越多,越便宜。(这点很好理解,不多解释)
零边际成本:再生产商品是没有成本的。虚拟的商品一般都有这特点,比如软件产品、媒体内容等。我们创造出来后复制的成本非常低。
网络效应:产品价值随用户数量指数增加。很多社交媒体产品如Facebook、Twitter、Instagram等,都具备这种网络效应。用户越多,产品价值越高,甚至能形成事实上的垄断效应。硅谷的一个肮脏的秘密是,很多获胜的企业是自然垄断。即使是共享汽车也倾向于一个赢家通吃的行业。只要Uber能运送更多的司机和更多的乘客,它的经济效益就会一直比Lyft好。像谷歌,你要搜索基本上没得选。我确实喜欢DuckDuckGo,因为隐私原因,但由于网络效应,它的搜索结果总是会差一些。Twitter,你还会去哪里发短文本?即使是YouTube,其网络效应比较弱,但它仍然足够强大,以至于没有第二家视频网站可以与之抗衡。甚至在电子零售业,Amazon Prime那种存储信用卡和信息的便利性也创造了一个强大的网络效应。
这三者的关系是:「任何生产边际成本为零的东西都有规模经济,而且具备网络效应」。
对于个人来说,好的商业模式就是虚拟产品或媒体这种生产边际成本为零的东西。
获得独特知识
Specific knowledge can be found by pursuing your genuine curiosity
什么是独特知识?独特知识不是一种具体可通过培训获得来的知识,独特知识让你产生了不可替代的效应。任何能通过培训而获得的知识最终都会被人工智能所替代,如果只掌握这些简单的知识,我们很快就会更廉价的机器所替代。
我们什么能赚钱?在某个事情上为什么是我赚钱而不是其他人?这就是独特知识的作用。
也许你的脑海里肯定是这样的想法,即一切知识都是可被教学的,一切都可以在学校里教。而事实并非如此,独特知识可以被学但无法被教。独特知识要么可能与你先天性格有一定的联系,要么与你后天在高度复杂的环境学习有关。
独特知识让你在特定的领域中有了专业的判断力。经典的例子是投资领域,很难通过简单的教学就能掌握了投资的诀窍。
那如何才能获取到自己的独特知识?答案是通过寻找自己的好奇心而发现自己的独特知识,独特知识并非大热门的报考专业,它与你自身有密切的关系,发掘你的天赋,让天赋帮助你获取独特知识。
打造个人品牌
Figure out what you’re uniquely good at, and apply as much leverage as possible
在古代,我们可以通过写诗书来获得个人品牌。现代,只需要一个博客、社交媒体账号、公众号或短视频账号,就可以获得 个人品牌。
找到自己独特的优势,并给予足够的杠杆,能产生不断积累的个人品牌。打造个人品牌的好处在于你的创作(知识产权)都是你的个人品牌资产,这些资产可以帮助你获得与人连接的机会,而这些机会可能让你赚钱。个人品牌也能降低人与人的信任成本。
过简单的生活
Live Below Your Means for Freedom
很多中产都曾或正在陷入消费主义陷阱。资本为了倾销产品,通过媒体给公众洗脑消费价值观,从而不断消费。而当你赚更多钱的时候,消费主义告诉你,是时候升级你的生活方式了。住更大的房子,买更好的车,买很多你本来不需要的产品。直到你陷入这种生活的泥潭。
我在玩现金流游戏的时候就发现,如果我的身份是医生、律师这种高收入的人群时,我很难跳出老鼠赛道进入快车道。因为这类中产的消费支出实在是太高了,而被动收入又很难覆盖生活支出,要维持这种中产的生活只有不停的工作。「我们都是欲望的奴隶」,如果你控制不了欲望,也就无法体会自由的感觉。
过简单的生活,做一个时间自由、财务自由与精神自由的人吧。
慢慢变富
There Are No Get Rich Quick Schemes
一夜暴富只是别人收割你的手法。凡是卖一夜暴富课程的人,都想赚你的钱让他们一夜暴富,所以这是一个零和游戏。财富是个不断积累的过程,如果突然给你一大笔钱,你大概率也无法驾驭它。我们要与财富共同成长,不断提升自己的认知,创造获取财富的机会,最终获得适合自己的财富。
相比一夜暴富,我更喜欢 慢慢变富,这种财富价值观也是一种 长期主义。
更新时间: 40个月前 版本: ab9ebded7
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我的投资之路:从迷茫亏损到系统构建与“策引”的萌芽
使用Beancount管理家庭财务
我的家庭理财规划
年轻上班族的系统化投资之路
玩CASHFLOW打造你的财务自由之路
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
构建高质量的信息输入渠道
25 July 2021 14:44 | 3243 字 | 15213 次阅读
信息输入 • 自我管理 • 自我提升
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
本文聊聊我的信息输入渠道。为什么要聊这个话题?是因为信息源是一个人在信息社会存在的基础。人是社会性的物种,一个社会人对信息是有强烈需求的,而信息源会影响你的行动决策。一个低效低质量的信息渠道可能会让你做出很多错误的决策,以至于会让你形成错误的认知。从认知的角度讲,高质量的信息输入渠道的建设是优先级很高的事情,但诡异的是,很多人却对此投入甚少,甚至选择性忽视掉。
我在 构建终身学习体系进行自我提升 中介绍过信息输入的不同渠道,所以这期邮件主要分享如何构建一个高效、高质量的信息输入渠道。
什么是信息输入
我对信息输入的理解是人对某个外界事件的反应行动,这定义包含三个要素:人、外界事件、反应行动。外界事件可以是一个客观物体的存在事件,也可以是对某个事物与人的评价或一个抽象的概念。总之人接触到这些事件后会产生一些反应行动。这些行动可能是经过大脑思考的交流,可能是一个收藏行为,也可能是一个待办任务的制定,但重要的是一定要有反应行动。如果某个外界事件对人毫无反应行动的产生,那就不能算是信息输入。
信息获取的方式
主动方式
主动获取信息很类似于健身,你需要不断的锻炼才能让身体变得更健康。
搜索技能:搜索引擎我一般用Google,如果没法使用Google的话我会选择用Bing或 DuckDuckGo 搜索中文信息。一些高级搜索技巧见 Google高级搜索。
订阅:订阅最重要的是找到你认可的作者,然后订阅他们的公众号、邮件列表或频道(杂志、专栏或视频)。我的订阅策略是通过一些搜索渠道或他人推荐的方式先找到一批种子订阅号,之后通过这些号的推荐找到更多的作者。
阅读:阅读是最古老的主动获取信息(知识)的方式,早期是书籍的形式,现在可能是博客之类的内容形式。在阅读的方式上,我建立了自己的书单。我按照关注的几个主要领域如技术、投资、历史及综合分类,通过其他人推荐或者主动搜索相关领域的推荐书单的形式,构建了我的阅读清单。之后设定阅读计划,通过听书或看书的方式吸收书里的信息。
被动方式
被动获取信息很类似于通过看电视购物里的减肥广告,期望获取躺着减肥的神奇方法。
算法推荐:现代互联网通过古老的机器学习算法构建了以短视频和推荐信息流为主的信息产品,这类产品类似于减肥药,吃多了容易反弹。推荐算法一个很大的问题会制造出围绕使用者的信息茧房,也就是用户最终看到的都是自己喜欢看到的信息,信息单一化让使用者看到的都是片面的信息,这对提升认知来说是个灾难。
社交信息流:社交是古老的被动获取信息的方式,比如你在朋友圈子或社交媒体上看到一些八卦的信息。这种信息获取方式最大的问题在于封闭的圈子,如何破圈是关键。举例来说,你是一个程序员,你周围的人大多也是程序员,由于共同的思维方式,你得到的信息也大多是同一类,信息在平流层流动。如果你想了解一些投资的知识,很难找到金融圈的从业人员。我的解决方法是通过不断学习不同领域的知识来认识一些我重点关注领域的人,比如我在学习金融知识的时候会考取一些投资资格证,以及写一些和金融相关的文章,看起来是种很慢的方式,但效果却不错。另外写作是破圈的利器,写作也是高质量的社交方式,作者很容易通过一篇文章破圈。
信息质量反馈机制
信息输入是一个墒增的过程,高质量的信息渠道需要不断的投入时间维护,砍掉无用信息的渠道,增加新的高质量的信息渠道。因为我们时间有限,能接受信息的带宽有限,提升信息输入质量的关键在于砍掉低质量的信息渠道,难点在于如何发现低质量的信息渠道。我一般采用以下几种方式解决这个问题:
全面获取信息,交叉对比:面对一个陌生的领域,第一步我会通过主动获取信息的方式获取到一定量的信息。然后对这些信息做交叉对比。比如一篇文章,如果我认可的多个作者都推荐,我会仔细阅读并获取更多的信息。如果一个观点,不同的人有不同的看法,我会进一步去思考是否接受或者优化这个观点带来的认知。正确的决策需要全面的信息,虽然这个全面也是相对意义上的全面,但对于进一步的行动来说足够就行。
区分事实与观点:事实是一件事客观的信息存在,虽然有时候我们无法得知真正的事实,看到的可能是别人精心准备的伪造事实。观点是人对事实的思考结论。这两者很容易混淆,尤其是在现代信息爆炸的互联网,我们很容易误把观点当事实。我的解决方法是找到不知道传了多少手信息的最早来源,通过一些搜索技巧,不少信息还是容易找到最早的来源。之后确定这个信息来源是观点还是事实,然后做出自己的反应行动。
正视偏见:偏见害死人,我们对自己坚持的观点很容易选择性忽略一些重要的信息,做出完全错误的反应行为。正视偏见也是正视自己,我们生来就带有很多偏见,我们生长的环境处处存在偏见,要做到完全无偏见的做事是一种修行。同样的信息对于带有不同偏见的人来说可以产生完全不同的认知,所以面对有对立的观点的信息,先不要立即下定论,而是确定是否自己对此观点持有偏见。
通过输出与人交流:信息输入好不好,很难直接评测。但通过对此信息的输出,与人交流观点,可以从他人处得知信息输入的质量好坏。通过输出的反馈去提升输入的质量。如果输入的信息源是错误的,我可以从持有正确输入信息源的人的反馈中得知,从而有机会去修正它。
基于信任度的信息输入渠道建设
我们在使用Google搜索相关关键词的时候,Google会对海量网页进行PR排名,排在第一的页面就是关于此关键词匹配度最高的页面。海量的页面拥有海量的信息,其中很多都是低质量的干扰信息。Google使用的是PR算法,简单的说就是指向网页的每个链接都是对这个页面的投票,这点类似于文献引用,不过存在的问题是垃圾页面可以通过伪造链接的方式欺骗搜索引擎。这点可以理解为热门的信息质量不一定高。
如何解决这个问题呢?2004年,雅虎和斯坦福大学发布了一篇名为 Combating Web Spam with TrustRank 的论文。论文提出从一系列被信任的种子网站(由人工审查选取)开始执行PR值计算。这种降低垃圾信息影响的方法就叫网站的信任度。
通过信任度来解决信息输入质量差的问题,比如我会设置一些重点关注的不同领域的作者列表,对于他们的推荐我会优先关注。
信任度的反面就是黑名单机制了,李笑来老师曾说过“ S.B. 的共识也是共识”,所以我有个黑名单列表,一旦通过一定时间的观察觉得此信息来源非常不可靠,我会找出和这个信息来源相关的来源,共同加入黑名单。
通过不断的信任与拉黑不同信息源,这个过程相当于训练一套高质量的信息输入系统,相比算法推荐这类弱人工智能的训练方式,这算是真人工智能的训练方式了。
除了黑名单外,还有一种奇妙的方法如下:
高效的信息渠道
付费和自动化是我构建高效的信息渠道的两种方式。
付费:好的信息值得付费,比如某些高质量的杂志或邮件列表,这种信息渠道已经经过编辑精心整理,非常高效的把有用的信息传递给人。
自动化:我会通过一些自动化的方式监控某些信息源,定时将信息通过邮件发送给我。自动化的方式有很多种,比如通过IFTTT或GitHub Actions自动抓取数据并发送提醒通知。
信息审查的影响
谁控制过去就控制未来;谁控制现在就控制过去。(一九八四)
信息审查会导致信息失真,一旦信息失真,会让我们做出不可预知的决策。解决这个问题的办法很简单,主动去搜索不同语言不同地区对同一件事的报道,对比分析大概能分析真实的事情走向。
虽然信息可以被篡改,但互联网有记忆。对于数字化的信息很难被彻底从整个互联网同时篡改了,所以找到这些不同时期的版本对比就能做一些分析。有个号称互联网时光机的网站叫 Wayback Machine,它会不断的爬取并备份整个互联网公开的信息,浏览器安装它的插件后当访问某个被删除的页面时会自动找到最近的归档版本,还能查看这个页面的修改记录。如果一个新的网页可能会被某些神秘的力量删除时,可通过插件备份整个页面,这样所有的人都能看到它的历史版本了。
广告
这篇文章是BMPI.dev第三期的邮件通讯,之前两期见这个 归档 。邮件订阅是一种古老而高效的获取信息的方式,甚至很难受到网络不可访问或内容审查的影响。所以如果你觉得本文不错的话,可以订阅我的邮件列表,一些敏感而不方便公开的内容我会选择邮件列表的方式发表。
更新时间: 30个月前 版本: 4a4a6beef
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我的AI阅读助手
OKR + GTD + Note => Logseq
我的人生管理系统
构建终身学习体系进行自我提升
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我的人生管理系统
14 March 2021 15:30 | 3410 字 | 40395 次阅读
个人成长
自我管理 • 自我提升 • 目标管理 • 时间管理 • 笔记管理 • 财务管理 • 纯文本 • OKR
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
我在 我的时间管理工具 与 我的笔记系统 中分享过我不断演进的时间管理工具与笔记系统。在使用这些系统多年后,始终有个困惑萦绕在我耳边,那就是:
单纯的记录时间、笔记和资金是一种碎片化的行为,容易导致过于追求细节而忘掉全局,让自己成为一个忙碌的战略懒人。
时间管理、记账、记笔记、学习、写作、阅读、交流这些行为都不是最终目的,它们只是一种实现个人长期目标的辅助方法,如果做这些事对长期目标毫无帮助,那没必要浪费时间去做。
虽然我做了大量的时间与资金开销记录,但我也很难评估这些开销对实现长期目标有何影响。这种困扰可以看作个人时间管理的哥德巴赫猜想:
我知道我的时间有一半都被浪费掉,但就是不知道哪一半。
想解决这个问题,就得简化问题的复杂度,这个问题其实等价于以下两个问题:
今天我做了啥?
做这些事对我的长期目标有什么帮助?
经过一段时间的思考,我有了以下的简单模型去解决这个问题。让我们先对问题进行建模。
问题建模
如果把人看作一个Machine,这个Machine有输入有输出。最初这个Machine只拥有时间维度,逐渐它拥有了技能维度,同时积累了金钱维度。如果把时间、技能与金钱看作它的输入端,那它输出端就是金钱、技能与影响力维度。其中金钱和技能在输入与输出间自循环,一般是正向的。我们的金钱和技能都会随着输入与输出的循环而提升,而影响力其实就是知识的变体,我们通过知识去影响别人,通过知识去建立权威与信誉度,最终发展出个人品牌。这个流程看起来非常像是一场 打怪升级的游戏。这些抽象的维度落地后就变为 时间管理系统(目标、任务与时间)、财务管理系统(记账、投资与保险) 与 知识体系(写作、笔记与个人品牌)。
想象这个场景:通过设定个人 OKR,把一个个 Key Result 拆解为一个个 TODO Task,用时间管理工具去追踪这些Task的时间开销,用财务管理去追踪这些Task的金钱开销,用知识管理去追踪这些Task的笔记,那我们是可以知道每个Objective的时间与资金开销,同时也能知道这个Objective的输出笔记产物有哪些,最终输出的影响力程度。通过不断优化输入与输出端,我们可以形成高效的学习能力,更好的适应外界的变化。如何优化?就需要记录输入与输出端各个维度的数据。通过记录底层维度数据来把控高层目标的实现。从时间与财务性价比的角度看一个个最终执行的任务,如果某个任务能更好的推动OKR目标的实现,那这个任务的投入就有价值。
理想的情况是,每个人拥有自己的输入与输出各维度的Dashboard,这样就能从宏观视角看到自己每天做的事情的价值。我们可以量化每件事情的投入产出比,去优化自己的处理流程。
life manage with input and output
系统设计
在对问题建模后,我现在的业务需求是:
全局的Tag系统。该Tag系统可以从个人愿景中设定年度OKR目标,之后可以对OKR的 Key Result 进行细粒度的Task拆分。
任务时间开销追踪。可以对OKR目标的某个Key Result统计相关的Task的总耗费时间。
任务资金开销追踪。可以对OKR目标的某个Key Result统计相关的Task的总资金成本。
任务相关笔记关联。可以对OKR目标的某个Key Result关联相关的Task的笔记。
Dashboard原始数据生成。可以自动或手动生成个人OKR完成度表现报告与财务健康度报告。
下图是一个从个人愿景到OKR ,之后从OKR生成每日待办任务,追踪任务相关时间、资金与笔记的流程图。
Life Tag System
系统实现
我的系统实现原则是尽可能用开源的工具组合实现我的需求。所以基本原则有以下几条:
工具需必须要开源。不考虑商业版的原因是未知不可控的商业风险,这些工具都是要长期使用的,选择开源方案是性价比最高的。
工具需要具备可扩展的能力。只有可扩展的系统才具备组合的能力,这样方便后期定制化的使用。
工具要有一个好的社区。一个好的社区非常重要,可以让我们解决问题的成本下降很多。也可以使用别人提供的现成解决方案。
本地优先。数据必须要存放到自己控制的存储(本地和网络都可以)中,隐私第一原则。
数据尽可能是纯文本。纯文本更简单,就算工具不可用,也可以直接对文本进行读取或修改。长期来看,这点很重要。
门槛低。最好一天上手,三天精通,我不想花费数周时间去学习一个复杂的工具。
现有解决方案
Emacs
如果直接选现成的方案,一定有人会提到这篇 Organize Your Life In Plain Text!。这篇超过10年历史的文章看起来如Emacs一样强大而久远。用Emacs做的确符合以上所有基本原则,问题在于我不是一个Emacs专家,我不想学数年Emacs后再开始解决这个问题。我的思路是花最多不过一周时间去用开源工具组合出一套解决我需求的方案出来。一周时间,我可能连Emacs快捷键都记不住。
自己动手做
当我看到这篇 Blog graph 和 Map of my personal data infrastructure 时整个人都震惊了。作者全部自己动手做了一套类似的系统去管理他的人生。如果我有时间的话我一定会这么尝试去做,但是我没时间。
工具选择
这套系统实现的关键在于需要有 Tag、TODO、笔记管理、记账等功能,记账可以用 Beancount 去做,只需要结合它的Tag功能就可以追逐相关Tag的开销。那么只需要选择一个具备 Tag 和 TODO 功能的开源笔记工具就行了。这么看来我们可选择的很有限了。
以最近大热的 Roam Research 为例,它开启了不少笔记工具的新特性:反向链接、右边栏、每日笔记及图数据库。最重要的其实是它的图数据库特性,因为当一个工具具备了数据库的查询能力时,结合插件特性我们可以做出非常多的应用场景。可惜的是它是一个商业软件且不开源( Notion 也因为这个原因被排除),所以我找到了一个具备它很多特性的开源软件 Logseq,Logseq本身受很多笔记工具的启发,如 Roam Research、Tiddlywiki、Workflowy 与 Org Mode 。
在把玩了几天Logseq后我发现最强的就是它底层的DataScript内存图数据库,可以通过高级查询做很多自定义界面。这样就可以满足我们的很多需求了,比如生成每日待办任务,根据OKR生成的Tag来过滤和OKR相关的任务,统计相关任务的花费时间等。
目前Logseq还在开发插件系统,等插件系统上线后,很多难以实现的功能都可以通过插件来完成目前Logseq已经实现了 插件系统,可以有更高级的玩法。当然我的需求还不需要插件系统的支持,光高级查询就可以满足。
我的方案
当确定了笔记工具和记账工具后,我最终的方案如下:
my life manage system
各工具的职责如下:
Logseq。负责制定OKR 、设定相关Tag 、拆分OKR相关任务并追踪任务耗费时间(时间管理)及管理这些任务相关的笔记(笔记系统)。
Beancount。负责记录相关Tag的资金开销,当然也是我的记账工具,最终帮助我生成家庭财务健康度的评估报告。
Tiddlywiki。负责公开笔记的输出。和Logseq的区别在于,我会将Logseq的一些笔记整理成可供分享的知识后发布到Tiddlywiki上。虽然Logseq支持公开某个页面,但是为了更好的区分私人笔记和公开笔记,我还是做了这个区分。
Amazon AWS。我的个人云平台。目前主要是用S3 + CloudFront做博客的图床。博客的页面访问数据 也是用 AWS Serverless 开发实现的。
Wayback Machine。类似OneNote和Evernote的Web剪藏工具。它可以永久保存网页的所有变更版本,所以一般无需自己把网页内容下载到笔记工具里,这样会导致笔记工具同步缓慢。我当时用OneNote剪藏了很多网页导致其存储达几GB ,每次换设备同步都很头疼。实际上只需要保存网页链接即可,最多加几句总结的话助记。有Wayback Machine再也无需担心网页突然不见了。
GitHub。项目托管与云存储平台。博客、记账与时间管理的所有数据包括文件(某些文件会放到Google Drive里)都存放到GitHub公开或私有仓库中。
Vercel。博客与Wiki托管平台。可直接关联GitHub后一键发布网站并自带CDN且 国内访问速度不错。
VSCode。文本编辑工具。一般我会用它完成博客写作、笔记写作(也可用浏览器)、绘图(也可用浏览器)、记账及代码开发。
Google/Twitter/YouTube。主要的信息获取平台。主动搜索用Google,被动获取用Twitter及YouTube。中文高质量的信息获取平台越来越少了,很多时候需要用英文才能搜索到我想要的信息。所以搜索技能和英文能力已经算基本能力了。
最右侧有颜色的三个框就是我的愿景最关注的三个领域了,我的所有输出都会和这三个领域相关。
实现效果
使用Logseq完成的最终效果如下图:
logseq
每天我都可以在Logseq系统首页看到以时间四象限划分法而划分的待办任务。具体如下:
📅 在做计划事项。标记为重要的或者和任何 OKR 相关的在做任务。
🌞 待做计划事项。标记为重要的或者和任何 OKR 相关的待做任务。
⏰ 在做其他事项。任何不重要的或者和 OKR 不相关的在做任务。
🚮 待做其他事项。任何不重要的或者和 OKR 不相关的待做任务。
底部的区块是和 OKR 相关的信息,让我可以快速了解到目前最重要的Objective有哪些。
具体的实现见 如何用Logseq实现OKR + GTD + Note 与 使用Beancount管理家庭财务。
本文配图皆用Excalidraw绘制,更多我常用的绘图工具见 我的绘图工具箱 这篇文章介绍。
更新时间: 35个月前 版本: 8171f612c
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
构建高质量的信息输入渠道
OKR + GTD + Note => Logseq
构建终身学习体系进行自我提升
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
构建终身学习体系进行自我提升
17 January 2021 34:50 | 7665 字 | 68428 次阅读
个人成长
知识管理 • 知识体系 • 终身学习 • 自我管理 • 自我提升 • 快速学习 • 知识系统 • 如何写作 • 如何阅读 • 笔记管理 • 个人品牌
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
为何要建立个人终身学习知识体系
建立个人终身学习知识体系流程
信息输入
如何找到优质的信息渠道
信息输入的不同渠道介绍
微信公众号
邮件列表
Twitter
RSS
书单
Google搜索
微信/电报群
1对1沟通
信息输入渠道金字塔
我的信息输入渠道
目标设定
个人愿景的设定
知识内化
如何管理时间
如何建立个人知识库
如何系统阅读
知识输出
写作的意义
如何写作
写作排版
写作渠道
博客/微信公众号/知乎专栏
Twitter/Telegram
Youtube/Bilibili
写作推广
知识应用
个人品牌
参考文章
为何要建立个人终身学习知识体系
在这个倡导终身学习的时代,我们已经掌握的知识很快就因为技术变革而过期。所以知识不仅需要系统的学习,还需要好的管理,最终形成自己的个人知识库。
之前我已经写过 人生游戏之路 与 如何快速学习一项新技能 两篇关于学习的文章。在这两篇文章中比较零散的阐述了关于「元学习」的概念,现在想想所谓的「元学习」不过是一套个人知识体系(或者 自我提升 系统) 。我们每个人都具备一定的学习能力,在不自觉中掌握某种学习的套路,只是有的人掌握系统化的学习框架,能够横跨多个领域去持续学习。
学习是一种我们生来就具备的能力:我们可以在学校中学习,也可以在社会中学习;可以在书籍里学习,也可以在与人交流中学习;可以自学,也可以在游历中增长见识。
我们可以从获取信息通过学习内化成自己的知识,也可以进一步将多种知识转变为自己认知的提高,不断提高的认知加上阅历的提升,最终可能成为某种智慧。 (本文只探讨从信息到知识这一环节。 )
为何要建立个人终身学习知识体系之学习的层次
从信息到智慧的过程中,学习贯穿整个流程。构建个人终身学习知识体系的好处,在我看来就是一个编排出适合自身的持续终身学习流程,它能帮助我在信息洪流中实现阶段性的人生目标。
建立个人终身学习知识体系流程
信息输入
每天都有大量的信息输入:
邮件订阅列表更新;
关注公众号文章更新;
博客RSS更新;
社交媒体大量Posts;
微信/电报群大量聊天信息;
GitHub有热门的Repo出现;
知乎有新的高赞回答出现;
某人有好书推荐给你;
Google搜索时发现好的文章;
培训课程推荐的资料;
与人沟通时获取的信息。
一不小心就会被垃圾信息喂养。现代社会不缺优质信息,缺的是如何找到优质信息。
如何远离低质量信息?我的一些通用的原则是:
越是大众的渠道信息噪音越高,信息质量越低,而小众的渠道或圈子质量比较高;
忽略那些没有基本科学素养的人提供的信息,可以用一些基本的常识来测试某个人是否可信;
高质量的信息很多都不是免费的,为知识付费是一种科学的行为;
谨慎追热点,热点信息里包含了太多低质量的信息;
事实只能被接受,观点可以去辩论;
如何找到优质的信息渠道
搜索引擎有种算法用来评估某个页面是否是高质量:通过一些可信的经人工审核高质量的网站作为种子网站,对于这些网站链接的网站会认为高质量,而这些链接的网站继续链接的网站也会被认为质量不错。当然也可以反向思考,如果一个网站本身被很多垃圾网站链接,那这个网站大概率质量也不高。这种算法类似一种 信任链传播 的算法。我们可以通过这种算法的方式 构建自己高质量的信息渠道。
中文世界的信息质量越来越差了。有多种原因引起这种趋势的变化,可看这几个推文进一步了解:
中文世界信息质量变得越来越差吗?
Worse Is Better
中文内容腐化的速度越来越快了,要获取到好内容需要的成本越来越高
我时常感到遗憾的一件事就是很多早期网络优秀内容的消逝
信息输入的不同渠道介绍
微信公众号
首先准备一些高质量公众号的列表作为初始的关注列表(可通过阅读它们的文章来判断) ,逐渐收集这些公众号推荐的其他公众号。公众号还有一大类是朋友圈和微信群推荐的,对于这类渠道优先选择小众的群里推荐的一些公众号做过滤。对于朋友圈,可以通过 一些基本的常识 来测试是否需要不看某个人的朋友圈,逐步提高朋友圈的信息质量。
对于好的微信公众号,我一般会用 微信读书 订阅,这样有新的文章会自动提醒我。
邮件列表
邮件列表我一般会订阅一些国外的技术周刊。一般是通过 Google 搜索相关技术资料时找到文章订阅地址。
邮件订阅列表可用 Gmail 的 filter 自动标记 label 并标记已读,这样可以减少大量的未读邮件。
Twitter
Twitter里我一般会关注一些List,比如 Elixir 和 SEO 这两个技术。这些List里包含了相关领域专家,可以获取最新高质量的一手信息。
RSS
一般是通过 Feedly 阅读器订阅。因独立博客的质量都还不错,总是能发掘一些不错的博主。
中文个人博客订阅列表:
中文独立博客列表
BlogHub
书单
微信读书与豆瓣豆列里经常可以发现一些不错的书单,我自己也会在Notion上整理一些书单:
综合阅读书单
投资阅读书单
Google搜索
主动搜索的信息一般要比被动推送的信息质量高,因为主动搜索是我们因某个需求而发起的主动搜索信息的过程。这里推荐掌握 Google高级搜索 技巧。
微信/电报群
大众群的信息大多数没价值,所以偶尔看看就行。小众群里反而有不少高质量的信息,进一步可以添加这些人为好友。这里的大众和小众的意思是相对的,比如技术群里很多 Java 技术群是没有价值的,而 Elixir 这种不热门的编程语言,圈子因为小,已经通过技术做了一层过滤了,所以群里的高质量信息比较多。
1对1沟通
如果能找到在相关领域有实战经验的人,与他们沟通往往胜过读很多该领域的文章。我有很多时候在读了某领域的书及很多文章后,反而有很多疑惑,通过与有该领域经验的人沟通后豁然开朗。
如何找到有该领域经验的人并且能建立与他们沟通的机会?这个问题我在下面 知识输出 部分会解答。
信息输入渠道金字塔
建立个人终身学习知识体系之信息输入渠道金字塔
如上图,信息输入渠道分不同层次,最底层的碎片化信息数量最多,但是质量有好有坏且不好区分。而中层主题阅读更适合系统性学习某个领域知识时,我们可以阅读该领域推荐的书单。最顶层的与人交流包含咨询、培训及与拥有该领域经验的人沟通。上面两层已经不算信息输入了,应该算知识与经验输入。
我在学习某一领域知识时遵循 自低向上 的过程:
因为一开始在该领域并不认识有该领域经验的人,也不知道读啥书比较合适,所以会先主动搜索该领域的一些文章先建立对该领域的 轮廓认识。
之后会关注一些该领域的公众号/知乎大V/邮件列表,甚至是看有没有Youtube相关的频道。
待感觉对该领域有了一个 比较全面的基本认识 后,我会找一些相关书籍去系统性的学习该领域的理论知识。
理论知识学完后找个场景去应用,去输出一些我自己内化该领域知识的文章,通过这些文章认识对该领域有更深入认识的人。
之后与这些人建立联系(包括平台私信、微信与邮件联系) ,做1对1的沟通,解决我在学习实践过程中遇到的一些难以解决的困惑。
不断的重复该过程,可以加深我对该领域的认知。
我在学习 投资 与 SEO 领域知识时,基本是通过上述学习流程而执行的。
我的信息输入渠道
以下仅是我常用的信息输入App清单:
读库:需要订阅,主要是非虚构类的文章。读库主编老六的品味不错,经常能看到一些有意思的人或事的介绍;
财新:订阅了财新通,有和读库的组合订阅。可以阅读财新周刊电子版(历年的都可以看),没订阅纸版是因为太贵了,数字版便宜还能在手机上看;
端传媒:简体中文新闻媒体能看的很少,繁体中文里端算较为客观的,能看到一些不同的视角报道;
今日热榜:可以订阅一些论坛社区的热门帖子,虽然简体中文新闻媒体很多没法看,但一些好东西可以在老的论坛社区看到。类似的网站有AnyKnew,还可以用RSSHub做自己的基于RSS的聚合阅读器;
narwhal:是一个我觉得不错的Reddit社区客户端,看帖子交互比较好;
图书:iOS自带的图书App,配合Z-Library能阅读海量的电子书。可以和macOS的图书自动同步,阅读英文电子书可以用自带的翻译工具大段落的翻译,阅读体验好;
Octal:Hacker News的第三方App,偶尔看看,平时还是通过邮件阅读每周热门的帖子;
YouTube:订阅了会员,没有广告观看体验好很多;
目标设定
给自己设定个目标很重要,甚至是个人知识体系最重要的一个环节。如何设定一个长期目标?可以从自身如需求、兴趣、性格、特长、时间等方面考虑,这是一个漫长的过程,我在确定 我的中长期目标 时也花了很多时间,甚至在确定后不断的微调。
构建我的被动收入 博客就是我在确定自己的中长期目标后建立的,作为追踪我中长期目标的一个平台。下面是我在设定目标时建立的一个公式:
终身学习(认知杠杆)* 全栈技术(时间杠杆)* 投资理财(财务杠杆) => 被动收入(人生杠杆)
这个公式结合了我擅长的领域和我感兴趣的领域,最终推导出我的中长期目标。
更进一步的是你可以像公司一样设定自己的个人愿景。
个人愿景的设定
愿景的设定决定我们个人做事的基线,它能帮助我们快速决策某件事,如果这件事于我们个人愿景有益,那就可以做,反之不会去做。我给自己设定的 三个愿景:
认知升级:不断学习去升级认知,做更好的自己;
长期主义:做有长期收益的事情,比如写作与投资;
积极分享:分享个人经验积极影响他人。
这三个愿景帮助我在自我管理及自我提升的道路上不断前行。每当遇到阻碍时,我就会从这三个愿景中找寻解决问题的方向,遇到分歧时,也会参考这三个愿景做最终决策。
认知升级决定了我会不断的学习新的领域去提升我的认知,做更好的自己;长期主义帮助我远离有短期收益的事情,人的精力有限,我不希望我的时间全部耗费在短期收益的事情,如做短线交易与给别人打短工赚钱;积极分享促使我不断输出自己的经验分享给别人,这种分享不是基于自媒体流量思维,而在于我的分享能给我带来一定的社交品牌效果,让我获取更多高质量的社交机会,认识更多具备领域经验的人并与他们做深入学习。
我的最终目标是构建我的被动收入,但是在愿景里并没有体现这一点。因为被动收入在我看来是结果而非过程,如果我能做到愿景里的这些,获取被动收入是很自然的事情。如果你以赚钱为目标,那实在是很糟糕的个人愿景和人生目标,因为它只是我们努力提升自我的副产物,而非我们的主要目标。
知识内化
在获取了大量信息后,我们需要对信息做整理内化,这个过程是信息到知识的一个中间过程,主要的处理流程是通过构建我们的笔记系统,合理的把大量的信息分类整理并建立双向链接,建立卡片式的知识片段,在我们需要输出某类领域知识时,我们可以快速找到这些相关的片段供我们进一步的使用。
在知识内化的过程中,主要涉及以下几个问题。
建立个人终身学习知识体系之知识内化需解决的三个问题
如何管理时间
自我提升及自我管理很重要的一个环节是如何管理时间,我们总是抱怨自己没有时间做某个事情,其实并不是没有时间,而是因为时间没有很好的被管理。
我在 我的时间管理工具 里详细阐述了我的时间管理流程,目前我的待办事项分为inbox、目标、学习(阅读) 、写作及个人项目五个维度:
.
├── inbox.todo
├── goal.todo
├── study.todo
├── write.todo
└── side_project.todo
建立个人终身学习知识体系之时间管理-inbox.todo
建立个人终身学习知识体系之时间管理-goal.todo
建立个人终身学习知识体系之时间管理-study.todo
建立个人终身学习知识体系之时间管理-write.todo
建立个人终身学习知识体系之时间管理-side_project.todo
几乎我所有的待办事项都使用目前这个以VSCode +文本+ Email的方式管理,我把收集的信息拆解到这五个维度里的某个项目中,对于正在做和优先级高的项目都可以在VSCode左侧的窗口看到,每天早上还可以收到正在做的事情的邮件提醒:
建立个人终身学习知识体系之时间管理-邮件待办事项列表
建立个人终身学习知识体系之时间管理-邮件待办事项内容
一些杂事都会放在手机日历中,尤其是一些定期重复的事项:
建立个人终身学习知识体系之时间管理-手机日历待办事项
目前这种时间管理的方式成本很低,数据都托管至 GitHub,可长期持续运行,不受具体软件的影响。
如何建立个人知识库
构建个人知识库可以很好的帮助我们进行个人知识管理。我在 我的笔记系统 中详细阐述了我的笔记管理流程。目前我的笔记管理主要以 TiddlyWiki 为主,部分还使用 Notion 管理。笔记管理的核心在于可以让我们以很低成本对知识碎片复用且很容易对这些知识片段打 Tag 并建立双向链接,在我们需要该知识碎片的时候很容易找到它。
建立个人终身学习知识体系之个人知识库-TiddlyWiki双向链接
TiddlyWiki这种以卡片( Tiddler )的形式组织信息,这些Tiddler又可以添加Tag,还可以设置双向链接。对于我组织整理笔记来说足够用了。
Notion我一般会放一些非公开的个人信息,还可以多人协作,适合家庭成员共同编辑。当然Notion的功能非常强大,已经超出笔记管理的范畴,你可以用它做非常多的事情,比如任务管理、电子看板、博客、电子表格等。
建立个人终身学习知识体系之个人知识库-Notion
我整理了一个 Notion的导航页面 ,你可以在里面看看别人是怎么玩Notion的。
如何系统阅读
阅读是一个非常大且复杂的话题,我不想在这里展开细讲,对于如何阅读多种书籍,可以看看《如何阅读一本书》。这本书里会详细介绍不同书籍的阅读方法。我用的最多的是检视阅读与主题阅读。
对于刚开始接触某个领域,先通过检视阅读多本基础书快速了解这个领域的轮廓。之后整理出主题阅读需要精读的书单,最后通过待办事项设定阅读计划,按计划逐步啃这些书,一般主题阅读可能耗时半年以上。比如我在技术提升目标方面设置的阅读计划如下:
建立个人终身学习知识体系之如何系统阅读
这个阅读清单近140多本书,几乎全方面涉及了我感兴趣的领域。投资领域里我也设定了近40本书的清单。对于这么多书我并不会全部阅读,这里有很多书是通过检视阅读读个大概,目的就是要更全面的去了解这个领域,防止遗漏某些方面导致作出错误的分析判断。最终主题阅读里的书不会超过10本,主题阅读的书单都是一些经典可以长读的书籍。
由于精力有限,所以我做的阅读很多都是功利性阅读,带有一定的目的去读某个领域的书,而非漫无目的的阅读。
知识输出
通过知识输出的方式去学习是更高级的终身学习方式。我们可以通过写博客、写Tweet、社区回答问题及录制视频等诸多方式去输出知识。
写作的意义
写作是深度终身学习:当你阅读时,你是被动接受作者的观点,你的大脑并未全面调动起来去分析问题。而一篇文章,需要你耗费很多时间查阅资料,甚至数年的经验总结。当文章发表后,可以与读者互动而相互学习,这种学习是一种深度学习;
写作是高质量的社交:什么叫无效社交?在群里灌水很难让别人信任你。而一篇好的文章会让读者与作者建立更深度的连接。作者可以通过文章去影响别人,如果这种影响是积极的,那这篇文章就很有价值与影响力;
写作能创造新的机会:在我写作的一些文章里,有一篇文章让我找到一个新领域的工作机会,有一篇文章让更有经验的人与我合作,有一些文章让别人信任我并给我推荐新的合作机会,有一些文章让我认识更多全国各地的朋友,这些都是无法用流量(阅读量)价值来衡量;
如何写作
写作最难的是开始动笔写,当你开始去写并且能坚持,这已经算解决了最主要的问题。如果你还犹豫是否要开始写,可以考虑下我上面介绍的写作的意义。
我一般会在日常阅读或思考时收集一些我可写或感兴趣要学习的主题,先把一些关键点记录下来,然后在有时间时选一篇开始动笔写。一般一篇文章有时候会在一小时写完,有时候会耗费几天甚至一周的时间去写。
在写的时候,我先会搜索下这个题材有没有人写(基于SEO的方式去写作) ,如果有的话大体有哪些热门的文章,先把这些文章简单浏览一遍。之后耗费半小时去罗列提纲,如果这个题材刚好在我的笔记有记录片段信息的话,我会把这些信息也给复制进来。然后开始思考是否需要绘制一些配图,如果需要绘制的话,先开始绘制这些配图,具体的绘图工具可以参考我这篇 我的绘图工具箱 。
之后开始进入写作流程,一般开始写整个人是懵逼的,很难进入状态。这时我会戴着耳机不断重复某首很熟悉的旋律歌曲,这种无限循环的歌曲可以让我大脑很快进入一种写作的心流状态。
在写作正文的过程中,基于SEO的写作模式,我会考虑是否可以与我之前写的文章建立一些链接关系。譬如这篇文章里有一些我之前写过的文章,这样可以把这些分散的文章串联起来,也会让读者对该主题了解的更全面一些(迫使他在你的网站上停留更长时间) 。
写作排版
排版规范可参考这篇 中文文案排版指北 。
也可以使用 赫蹏 这个库自动对内容进行优化排版(本博客目前中英文混排使用的方案)。
如果使用VSCode编辑Markdown的话还可以安装这个 Pangu-Markdown-VSCode 插件,在写完文章后一键自动排出上述规范要求的效果。
文章配图可以使用 TinyPNG 压缩后上传,这样不仅可以使网页加载速度变得更快,还可以降低你CDN和用户的流量费用。
如果你对中文排版设计感兴趣,还可以看W3C的这篇 中文排版需求。
写作渠道
建立个人终身学习知识体系之知识输出渠道
我在 零成本搭建现代博客指南 这个系列里写了几篇关于如何搭建个人博客的系列文章,感兴趣的可以看看。
博客/微信公众号/知乎专栏
对于长文,我的发布平台是本博客、微信公众号及知乎专栏。
一般正文写完后,在 Unsplash 上找个封面配图,之后 一键推送 到GitHub,自动触发发布流程,文章就上线了。
知乎专栏在博客文章上线后,直接复制粘贴到知乎专栏编辑器就可以了。而微信的排版是用Markdown原文粘贴到 微信公众号在线排版工具 ,然后复制到微信公众号编辑器,所有配图还得单独上传上去。
Twitter/Telegram
Twitter 里我一般会发一些简单的短文。也可以选择在长文里复制一些比较精彩的单独发布到Twitter。
Telegram里我创建了三个不同主题的Channel:一个关于 自我成长 ,一个是关于 被动收入 ,一个是关于 流量研究 。
Telegram里的玩法比较多,这三个主题的Channel里的消息通过机器人转发又会自动汇合至 学习&技术&投资 这个电报群里。在这三个Channel里发的消息也会自动通过Twitter发布出去。考虑到SEO及某些神秘的网络原因导致无法访问的需要,我又创建了一个 电报频道静态化工具 ,这个工具可以自动将三个Channel里的消息自动发布到我的博客这个 思考 页面里。
Youtube/Bilibili
视频的制作成本很高,在制作了几次质量一般的视频后,我暂时放弃了视频类的发布。目前还是写文字为主,未来等找到低成本制作视频的方式后,我可能会继续尝试以视频的方式输出知识。
写作推广
文章写完并发布后,最头疼的事情在于如何推广:推广就是让别人知道你写了篇牛逼但还没有人看的文章。我一般选择去微信群、朋友圈、V2EX、Twitter及Telegram里写一段简介(一般是网页描述标签的内容) ,然后附带文章链接发布。之后看别人是否吐槽还是点赞,然后与他们(可能愉快)的交流下文章心得。当然如果没人响应你也无所谓,不要气馁,继续坚持写下去,没人看不代表你的文章质量不好,很可能是取了一个令人无感的标题或者选题过于缥缈导致曲高和寡。
在推广时,如果有好的配图记的附上,比如在Twitter上你可以把文章里精彩的配图附上,好的配图比标题更吸引人。
知识应用
知识输出只是帮助我们从理论上系统化的学习,而直接做一个相关的项目会帮助我们更深入的理解该领域知识,甚至知识的意义也在于有具体的场景应用。我们的工作就是利用我们所学的知识转化为具体的应用。只有在熟练应用某领域知识后,我才能确定我真的掌握了此领域知识,也就是获取了该技能。
由于我是一个软件工程师,我的很多技能都是和软件相关的,所以我的项目都是在GitHub上托管的。一般处于idea阶段的项目我都会记录在待办事项中。而处于正在开发的项目都会托管在 GitHub 中。完结的项目都会记录在 个人项目 中。
如果你要掌握某个领域知识,最好的方式就是做一个相关的个人项目,你可以独立完成或者参与已有的项目。
idea不重要,重要的是具体的做法。一旦我有了某个让我产生幻觉的idea,我都会去搜索一番,很大的概率是别人已经做了。不过这不重要,可以看看别人是如何做的,借鉴改进它。
个人品牌
当我们通过终身学习不断的去实践应用,最终就会走到创造属于自己个人品牌的那一步,这也是我认为学习的最终阶段。关于这部分可以进一步阅读这两篇文章:
打造个人品牌·财富常识
从内容创作到个人品牌
参考文章
捕蛇者说:个人知识管理体系系列
如何搭建属于你自己个人知识体系1.0
更新时间: 34个月前 版本: cdf2af7f7
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我的AI阅读助手
构建高质量的信息输入渠道
OKR + GTD + Note => Logseq
我的人生管理系统
我的笔记系统
人生游戏之路
如何快速学习一项新技能?
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我的家庭理财规划
14 January 2021 14:45 | 3245 字 | 13365 次阅读
理财规划 • 投资规划 • 家庭财务 • 保险规划
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
理财不只是投资。
在继续看本文之前,先问自己一个问题:你需要赚多少钱才能过好生活?
系统性的理财规划流程从采集客户的信息(基本信息、家庭财务信息、风险属性评估、投资组合持仓、保险信息、理财目标)开始,然后对客户家庭财务进行诊断,测算现金流量,进行资产配置,之后分别对不同的理财目标(子女教育、自由梦想、居住规划、购车规划、退休规划、保险规划)进行现金流模拟,最终进行生涯仿真,绘制出家庭生涯仿真现金流表。
这是一个很复杂的过程,如果要普通人按这种方式去做理财规划,显然是不现实的,你甚至需要报考一门AFP(金融理财师)认证考试才能完全理解这些流程涉及的细节。
家庭理财规划表
回到最初的问题:一个家庭到底需要赚多少钱才够用? 这个问题理财规划是可以回答的。考虑到原始理财规划的复杂性,我把这个过程简化了,流程虽然精简不少,但是目的在于通过科学规划去完成家庭的多个理财目标。
家庭财务规划一部分和现金流游戏很类似,所以推荐你先看玩CASHFLOW打造你的财务自由之路这篇文章。
分析家庭财务状况
我在我的财务管理方案这篇文章中详细介绍了家庭财务规划如何制定。一般就是制定家庭收入/支出和现金流表,之后制定资产/负债表,如下图:
这里面月现金流很重要,它会影响我们能不能实现我们的理财规划目标,你可以把它理解为统计局常说的可支配收入。一个低现金流的家庭财务是很不健康甚至危险的,这意味着家庭结余的资金非常少。如果没有多余的钱,那我们也无法进行下一步投资规划了。
以下是我在Notion上制作的2020年家庭损益表和资产负债表:
家庭财务诊断
从上图可看出家庭财务需要提高储蓄、降低负债(和房贷有直接关系)。财富增值能力和财富自由度需要通过投资规划去提升。
设定家庭理财目标
我们可以为家庭设定这么些目标:
购房/换房目标
子女教育
赡养老人
购车目标
休闲娱乐
退休养老
财产传承
我们以子女教育为例:
如上图可看出子女教育随时间所需要的现金流。对于每个目标我们都可以通过估算每年所需的金额,将这些目标所需要的金额相加,最终绘制出家庭净现金流量表(以下数据为模拟数据,且理财目标包含了单个子女教育和赡养老人):
如上图,红色区域的现金流为负,而之前的现金流为正,这意味着家庭财务在经历一段时间的积蓄后(比如工作时净现金为正,而退休后净现金流为负),因多个理财目标的消耗,逐渐变成负的了,这时家庭理财的资金缺口就出现了。以上图模拟数据为例,该家庭在第26年时净现金流为负,前25年积蓄了近100万的资金,而之后21年里所需要近200万的资金,这就产生了累计100万资金的缺口(以无风险利率3%折算现值为53万)。
如何解决这个100万资金的缺口,正是我们需要构建家庭投资组合的原因。
构建家庭投资组合
继续以上面的模拟数据为例,该家庭在前25年积蓄了近100万的资金,如果构建一个年复合回报率为10%的投资组合,则在开始需要9.2万初始资金即可。但是该家庭直到第15年时才积蓄了10万块钱。为了简化计算过程,我们以定投来估算,每年投入1万块钱,连续投入25年,投资组合的年复合回报率为10%,则最终在第25年结束时我们有总资产为108万:
而本金只累计投入25万,对该家庭的现金流来说没有啥压力,完全可以执行下去。现在的问题是如何构建一个年复合收益率达10%的投资组合。投资短期收益率超10%非常容易,但是持续25年还能达到年复合10%的难度并不简单。
我们知道股票代表着企业,而指数代表着某个行业或者某个市场维度,所以投资组合长期收益率和国家GDP增速有着一定的关系。国家GDP增速代表着国内众多企业的平均增速,而股票市场里的企业大部分都属于头部优质企业,增速是要比GDP增速平均值要高。
杰里米·西格尔教授曾实证过,从长期来看,美国股市的收益率在扣除通货膨胀以后为5%—6%。以道琼斯工业平均指数为例,在1978年到1982年的四年时间里,道琼斯工业平均指数一共上涨28.6%,平均每年涨幅7.15%。从长期的角度来看,股票的平均涨幅一般应该是每年9%—10%。因此,如果投资者将他们的投资组合管理得不错,应该可以获得每年12%—15%的复合投资回报率。(摘自《在苍茫中传灯:一个投资者20年的知与行》)
这是发达资本市场的长期回报率,而中国的资本市场的长期回报率大概是多少了?在《中央国民经济和社会发展十四五规划和2035年远景目标建议》这篇报告中提到:
预计2035年我国GDP规模将较2020年翻一番,超过200万亿元。
这意味着从2020年到2035年这10年中国的GDP需要以年化4.7%左右的速度增长。同时参考一个简单的资产配置方案 和做好两个长期觉悟这两篇文章,我们可以假设宽基指数的平均回报率大概是7~8%的水平。
这意味着如果长期持有宽基指数如沪深300和中证500此类指数的长期回报可以达到7-8%的水平,结合一定的策略如估值和双均线,做到在便宜的时候买入,在高估的时候卖掉,我们有理由相信这类投资组合策略可以达到长期10-15%的投资回报。
我在200万小目标组合月报(202006)这篇文章公开了一个计划长期投资10年以上的指数基金投资组合:被动收入投资组合,目前该组合运行了两年多,年复合回报率达18.49%(截止2020年12月底)。
制定家庭保险规划
为什么要买保险?保险是智商税吗?
我们知道保险的首要目的是为了抵御未知的风险:如家庭主力突然因为重大疾病或意外导致家庭收入暴跌,无力偿还长期债务如房贷等,一旦遇到这种问题,对家庭的打击是毁灭性的。而为了抵御这类风险,保险应运而生。所以买保险最重要的是防御而不是进攻,放在实际操作中就是买消费险而非投资分红险(万能险)。如果你买了投资分红险如万能险或者返还型保险,大概率你的保费会被保险公司投入风险更高的市场去盈利,而给你的利率甚至都不如存银行,要知道保险公司养了一堆精算师,每天研究的就是这个,想占你便宜不要更简单。
如何构建一个家庭的保险组合,只需要掌握几个简单的规则:
买消费险;
保额可以家庭总负债为锚,如家庭总负债200万,可以买总保额达200万的寿险和意外险;
给家庭主力买寿险+重疾险+医疗险+意外险;
给其他家庭成员可以优先买意外险+医疗险+重疾险;
上城镇/城乡居民基本医疗保险/新型农村合作医疗,也就是医保肯定要上,性价比高,有医保买其他商业保险也会便宜不少;
可以考虑给房子买个财产险;
每年保险费用占比最好不超过家庭总收入5%,但也不能太少;
买保险要趁早,越到后期身体很容易体检出问题,到时候保险不一定能买成,而且费用会很贵,建议在30岁之前就把寿险+重疾险买上;
买保险可以在支付宝上买,更容易追踪,它里面的产品一些还不错;
如果体检出一些保险不保的病,并不意味着不能买此保险了,某些重疾险如果提前告知体检的问题,它还是可以保其他疾病的,这方面可以提前咨询保险公司告知;
父母年纪大,很多保险买不成,可以考虑买支付宝的相互宝。最重要的是定期体检,很多癌症早期是完全可以治愈的,费用也不会很高;
如果公司也给员工买团体险,还要不要自己买商业保险?我个人还是建议自己买,因为一旦你离职,这个保险就没了,而你自己要买重疾险和寿险这类比较特殊的保险,年纪大了再买就很麻烦了。
以下是我个人买的一些商业保险(不包含医保):
确定家庭医疗规划
保险只能解决钱的问题,解决不了健康的问题。一旦人得了大病,最后都会很后悔当初为什么没有定期做好体检。
丁香医生这篇20岁开始,各年龄段都该看的癌症筛查清单,建议收藏!文章建议大家都看看,并制定自己家庭的体检计划,我的家庭医疗规划是:
家庭成员每年定期做基本的体检;
对不同年龄段的人做相应的癌症筛查体检;
定期追踪;
以上是我在Notion做的家庭成员体检追踪清单,把每年需要做的体检项目提前规划好,对于每年的体检报告和检查出需要定期复查的项目都做一个持续的追踪。
看到此处,相信我已经解答了开头提出的问题。
理财规划不只是投资,更是一种家庭健康的生活方式,这种生活方式给予我们健康的财务与身体,在未知的世界航行中,为我们的小家安全护航。
更新时间: 53个月前 版本: 5750fdef2
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
财富常识
使用Beancount管理家庭财务
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我的2020
18 December 2020 14:38 | 3221 字 | 7569 次阅读
年度总结
年度总结
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
我是一个终身学习与长期主义者,这意味着我做事的流程是通过持续学习某领域知识最终达到长期获益。如果把这个流程拆解为输入与输出两个阶段,那就是通过大量阅读学习新的知识,通过写作与各种实践完成知识的转化。
2020年我从阅读、写作、个人项目、关注领域与影响力五个维度总结,记录下这不平常的一年。
阅读
阅读的书籍主要是纸质书,一些书籍也是通过上下班开车时通过微信读书听的,偶尔也会听一些Podcast频道。以下是今年读过的书单:
技术:
Google SEO指南
SEO艺术
Thinking in Java
深入理解Java虚拟机
Java并发编程实战
Effective Java
设计模式
Spring实战
Spring Boot实战
重构
代码整洁之道(程序员的职业素养)
敏捷软件开发(原则、模式与实践)
修改代码的艺术
Java测试驱动开发(TDD)
咨询的奥秘(寻求和提出建议的智慧)
咨询的奥秘(咨询师的百宝箱)
人件
持续交付:发布可靠软件的系统方法
微服务设计
Spring微服务实战
领域驱动设计
实现领域驱动设计
高性能Mysql
投资:
一个投资家的20年
ETF全球投资指南
指数基金投资从入门到精通
今年读的技术书偏多,主要是想系统性的提升下编程技术,包括Java编程能力、软件工程实践、系统架构能力、算法应用、Serverless应用、SEO优化及机器学习应用等方面,目前只读完了Java编程能力及软件工程实践方面的书。
在投资方面只读了三本书,是因为之前也曾系统性读了十几本投资方面的书,具体书单见投资理财书籍推荐。
如果要选出一本对我影响最大的书,我会选择《一个投资家的20年》这本书,正是这本书促使我计划公开且长期运营被动收入投资组合。
如果要从中选择推荐的书,我会选择:
《SEO艺术》:如果要学习 SEO 相关的技术,只需要看这本书。
《修改代码的艺术》:我们总是在工作项目、开源项目中遇到遗留系统。在遗留系统中开发新的功能而不影响旧有的功能,尤其是很多遗留系统并没有足够的测试,这难度堪比在万米高空的飞机中替换发动机,一不小心就会坠入万丈深渊,而这本书就是你的降落伞。
《重构》:Martin Fowler 的这本书让我意识到好的代码不是一次就能写好的,就像好的文章需要千锤百炼,好的代码需要不断的重构,通过重构逐渐提高代码的可维护性。
《深入理解Java虚拟机》:读完这本书让我对JVM这个平台有了更深的认识,周志明老师的另外一本书《软件架构探索:The Fenix Project》也不错,目前正在看。
写作
综合知乎专栏、微信公众号(id: improve365)及构建我的被动收入博客的文章阅读量,2020年Top10的文章是:
我的笔记系统
什么是互联网广告
我的财务管理方案
零成本搭建现代博客之SEO优化篇
零成本搭建现代博客之搭建篇
用Markdown可以做什么
什么是SEO
Serverless应用开发小记
使用AWS Lambda提高网站图片加载速度1X倍
我常用的浏览器插件
看起来热门的文章主要是一些个人经验介绍类或知识科普类的。
个人项目
今年做的几个开源项目:
vscode-todo-plus-plus:我修改的一个vscode Todo++ 插件,具体见这篇《VSCode插件开发小记》。
todo_parser_lib:解析TODO格式的Parser库。这个库帮助我将本地的TODO事项转换成每日邮件定时提醒,具体详见《Parser黑魔法》这篇文章。
tg2web:这是我为自己做的一个小工具,可以把电报群组的消息自动发布成网页,具体效果可以见本博客思考。这个网站内所有的内容都来源自我的几个电报群组,每次执行这个工具会拉取电报群组内的消息并渲染成静态网页,然后发布至AWS S3桶,此桶绑定一个域名,之后就可以在网站看到最新的电报消息。
scrape_google_keyword:爬取Google关键词搜索结果页的试验库,用于SEOer分析Google SREP。
invest-alchemy:基于AWS的每日提醒ETF双均线交易策略信号的Serverless程序。具体详见《Serverless应用开发小记》。
invest-assistant:投资组合助手。IaC基于pulumi与ansible ,数据库为Postgresql ,后端服务为elixir/phoenix 。目前已经把基础设施IaC化:目前可做到一键发布应用系统至AWS EC2服务器,包括服务器创建、VPC安全组、域名、证书、性能调优、Nginx、安全、应用打包部署、CI/CD、数据库用户/库及权限的设置。通过一些DevOps的最佳实践,我只需要专注业务开发即可。替换一套新的环境成本很低,无需手工部署服务器及中间件。
我长期关注的领域
技术的低成本、高效率、规模化、结合SEO在Web开发上的应用
这需要选择合适的Web开发技术栈,我选择了Elixir/Phoenix、Python、JavaScript等;
这需要使用自动化(DevOps)的技术,如IaC、CI/CD、日志监控等;
这需要使用云原生(Cloud Native)的一些技术,如Serverless、AWS等;
这需要了解一定的SEO技术,包括爬虫、Niche研究、数据分析、规模化低成本的存储与展示等。
ETF指数投资
投资组合长期实盘运作;
投资策略交易信号的自动化提醒;
交易系统评测;
指数择时(估值)研究;
市场热度研究。
SEO
今年做的和 SEO 相关的网站/流量站:
bmpi.dev
我的个人博客,虽然做过一些技术性的站内SEO优化,但是SEO最主要的是有足够多的优质内容。而个人博客由于文章数量少,覆盖的关键词及这些关键词的月搜索量可能都不多,需要很长的时间积累链接权重,逐渐提高搜索流量。
从 Google Search Console 上看总展示次数和搜索点击次数在逐渐提高。
webtg.org
长尾流量增长势头不错,由于技术原因,目前内容生成遇到一些问题,希望能在21年解决这个问题。
gitopx.com
这个相比之下数据比较差,可能还是内容更新停滞导致的问题。
投资
目前实盘运营的被动收入投资组合今年的表现不错:
组合自2018年09月开启截止到2020年11月底:
沪深300自3310.13点到4960.25点,涨幅49.85%;
香港恒指自26435.67点到26341.49点,涨幅-0.36%;
标普指数自2930.75点到3621.63点,涨幅23.57 %;
本投资组合净值从1到1.532,涨幅53.2%。
这个投资组合全部投资于ETF指数基金,大概几个月调仓一次,指数基金的管理费也很低,总运营成本非常的低。年平均符合增长率(CAGR)能超过15%达到19%以上,这个成绩更多的得益于今年A股市场整体处于上涨趋势中。
被动收入投资组合的主要特点:
长期投资:唯有长期投资才能消除短期预测市场带来的赌博效果;
分散持仓:黑天鹅的存在让持有单一投资标的获得成功成为一种小概率事件;
简单策略:简单的策略操作成本低,更适合上班族,这也是组合名被动收入的来源。
今年各大指数中涨幅最高的是创业板,涨幅达46.37%;最少的是恒生指数,涨幅为-6.56%。
今年被动收入投资组合的净值只涨了10%多,与各大指数相比排名倒数第二,分析原因应该是年初的回撤控制没有做好,导致产生了超过14%的回撤。本组合的夏普比率为0.22,这个只能说控制风险做的一般,好的主动基金夏普率可以达0.7以上。
好的投资组合在牛市可以跟上宽基指数的涨幅,在熊市可以规避大的回撤,希望在未来的市场行情里本组合能做到这一点。
影响力
我的一个愿景是分享个人经验积极影响他人,通过分享与他人建立有效的连接,创造新的机会。我不以关注人数的多少为指标,我更看重的是因为我的分享能够对他人产生积极的影响。但是从量化的指标看,目前也只能以关注人数作为一个维度来衡量影响力。
个人博客:今年博客的新用户是20073人。
知乎:8235人。
微信公众号:id:improve365,关注人数2256人。
微信群(学习&技术&投资):240人。
Twitter: 194人。
Telegram: 291人。
Youtube: 14人。
Bilibili: 27人。
雪球: 219人。
我的主要发布平台是个人博客、知乎、微信公众号、Twitter及Telegram:
个人博客的自由度是最高的,可能国内访问速度比较慢,这是因为网站部署在国外,无需备案,所以博客也算一个备份的平台;
知乎算是一个内容发布平台,因为其更容易被Google收录,它域名的链接权重也高,知乎的流量也大;
微信公众号和知乎差不多,也是因其流量高,很多人喜欢在上面看内容;
Twitter上我一般会发一些碎片化的思考或者信息,其内容发布也更自由;
Telegram上我也一般会发一些碎片化的思考,或者收集文章的分享。
更新时间: 40个月前 版本: ab9ebded7
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我的2023
我的2022
我的2021
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我的绘图工具箱
8 December 2020 13:29 | 2969 字 | 21510 次阅读
技术写作
绘图工具 • PlantUML • Sketch • PPT • Excel • draw.io • Google Drawings • OmniGraffle • Python Jupyter Notebook • 时序图 • 架构图 • 部署图 • 线框图 • 甘特图 • WBS • 思维导图 • 白板绘图
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
由于在写作中经常需要配图,本文分享下我常用的绘图工具以及如何存储并展示配图。
白板绘图
Excalidraw
白板绘图的好处在于灵活度非常高。好的白板工具可以很方便的绘制自由的图片和预定义的结构化图形,这方面我在用的工具是 Excalidraw,由于是本工具是开源的,我也部署了自己的Excalidraw 白板工具。在 基于纯文本的人生管理 这篇文章里所有的配图都是用Excalidraw绘制,比如下图:
Excalidraw
一个好的白板工具应该需要有什么特性呢?Excalidraw的创始人写了一篇很好的文章 Rethinking Virtual Whiteboard 来回答这个问题。
Excalidraw的优点有以下:
无需注册,直接使用,用完即走;
代码开源,免费使用,不用担心商业问题;
多人实时协作,共同绘制;
数据可导出,可通过链接直接分享给任何人;
数据存储在浏览器本地,文件可离线编辑;
扩展性强,可以导入其他人绘制的资源库;
数据采用端到端加密,服务器端数据加密存储,隐私保护很可靠;
保存的文件可直接在VSCode编辑(通过插件),同时可被其他网站嵌入使用,比如不只是笔记工具的 Logseq 里的绘图功能就是嵌入Excalidraw完成的;
拥有这么多特性,用Excalidraw能画啥图得取决于你的想象力了:在 One Year of Excalidraw 与 Create Diagrams That Look Like Hand-drawn 这两篇文章里可以看到使用Excalidraw画常见的结构图如架构图、结构图、流程图与交互图等,也可以自由绘制如品牌Logo、事件风暴图、原型图、对比图、手绘图、四格漫画与平面图等。
除了Excalidraw外,其创始人早期用了另一个白板工具近10年,这个工具是 zwibbler,我看了下感觉用Excalidraw完全可以替代它。
Miro && Mural && Figma
这几个在线白板很适合团队协作(比如敏捷工作流如Retro、站会、头脑风暴、工作坊等),和Excalidraw不同之处在于,它们支持的模版更为丰富,还能集成不同的App,比如在白板里插入表格和图片之类。其中Figma虽然是个类似Sketch的设计工具,但是也支持白板的用法。
不好的地方在于它们都不是开源的,但是也提供了免费版。当然也可以用Google出品的 jamboard 替代,虽然没有提供模版,但是基本功能都类似。
Perfect FreeHand
带有压感的在线手写板工具,以下是用手写板手写的效果:
文本绘图
文本绘图的好处在于修改、存储方便,很简单即可绘制出固定模式的图,如时序图、架构图、部署图、线框图、甘特图、WBS与思维导图。 PlantUML 是一个支持文本绘制类UML图的工具,可以在 Real World Plantuml 找到很多用它实现的图。我一般用它来绘制以下一些图。
文本如下:
@startmindmap
skinparam monochrome true
除了PlantUML外还可以用 Markmap,相比PlantUML的好处在于它基于Web,不需要通过渲染生成图片的方式就可以从文本生成可交互的思维导图。比如下面的通过Markdown生成思维导图的例子:
inline code |
---|
1console.log('code block');
通过此库最终生成的效果:
inline code
- js console.log('code block');
- Katex - \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}\)
## 当然也支持中文啦 ¶架构图
源文本见 arch_aws.plantuml。
除了PlantUML外还可以用 Mermaid 来通过文本生成各种UML图。比如下面通过文本生成时序图的例子:
sequenceDiagram
autonumber
张三-»李四: Hello John, how are you?
loop Healthcheck
李四-»李四: Fight against hypochondria
end
Note right of 李四: Rational thoughts!
李四–»张三: Great!
李四-»王五: How about you?
王五–»李四: Jolly good!
生成的效果图如下:
王五
李四
张三
王五
李四
张三
loop
[Healthcheck]
Rational thoughts!
Hello John, how are you?
1
Fight against hypochondria
2
Great!
3
How about you?
4
Jolly good!
5
GitHub的Markdown已经支持Mermaid了,相比PlantUML的优势在于其原生支持Web,无需通过生成图片即可集成到网站页面中。
如果想更灵活的绘制复杂的架构图,推荐使用PlantUML底层使用的graphviz工具,它的效果可参考这篇Graphviz and Hugo文章。甚至可以用它来绘制RFC协议标准里的ascii风格的文本配图,比如下面的例子:
digraph {
subgraph cluster_0 {
a0 -> a1 -> a2 -> a3;
label = “process #1”;
}
subgraph cluster_1 {
b0 -> b1 -> b2 -> b3;
label = "process \#2";
}
start -> a0;
start -> b0;
a1 -> b3;
b2 -> a3;
a3 -> a0;
a3 -> end;
b3 -> end;
}
会生成如下ascii文本图形:
+- - - - - - - - - - - - - +
' process #1 '
' '
' +-+ +-+ ' +-+
+- ’ | a1 | < | a0 | ’ <– | start |
| ’ +-+ +-+ ’ +-+
| ’ | ^ ’ |
| ’ | | ’ |
| ’ | | ’ v
| ’ | | ’ + - - - - - - -+
| ’ | | ’ ’ process 2 ’
| ’ v | ’ ’ ’
| ’ +-+ | ’ ’ +-+ ’
| ’ | a2 | | ’ ’ | b0 | ’
| ’ +-+ | ’ ’ +-+ ’
| ’ | | ’ ’ | ’
| ’ | | ’ ’ | ’
| ’ | | ’ ’ v ’
| ’ | | ’ ’ +-+ ’
| ’ | | ’ ’ | b1 | ’
| ’ | | ’ ’ +-+ ’
| ’ | | ’ ’ | ’
| ’ | | ’ ’ | ’
| ’ | | ’ ’ v ’
| ’ | +-+ ’ ’ +-+ ’
| ’ +-> | a3 | ’ <– ’ | b2 | ’
| ’ +-+ ’ ’ +-+ ’
| ’ ’ ’ | ’
| +- - - - - - - - - - - - - + ’ | ’
| | ’ | ’
| | ’ | ’
| | ’ v ’
| | ’ +-+ ’
++–> ’ | b3 | ’
| ’ +-+ ’
| ’ ’
| + - - - - - - -+
| |
| |
| v
| +-+
+–> | end |
+-+
可以通过graphviz的命令行工具来生成,也可以使用这个在线网站:dot-to-ascii。
关于如何用文本画好一幅好的架构图,推荐看 C4 Model 的作者的这篇 Software architecture diagrams as text。
专业绘图
ASCII
ASCII风格的绘图很适合放在代码或纯文本文档中,如:
手工画显然太困难,可以使用相关的工具绘制:
asciiflow是款功能简单的免费在线绘制ASCII风格图的网站。
monodraw是Mac上功能强大的付费App,可以绘制复杂的ASCII图形,如下图:
Sketch/OmniGraffle
源文件见 Serverless.sketch。
要想用Sketch画好配图,配色很重要,这方面可以看draveness写的这篇《技术文章配图指南》。当然Sketch可以做的远不止这些,不过这不在本文讨论范围。
draw.io/Google Drawings
这类工具都可以绘制各类你想要的配图,唯一限制的就是你的想象力了。
在线绘图工具如 Google Drawings 的多人实时协作功能更是团队协作绘图利器。这篇《分布式事务中的时间戳》中的配图就是用Google Drawings绘制的。
代码绘图
Plotly
Plotly是一个基于d3.js与stack.gl的图形库,它可以用来绘制各种类型的图表,比如散点图、折线图、饼图、柱状图甚至3D图形等。
与Hugo的集成可参考这篇Plotly & Hugo。
如果是想基于各类数据库做商业数据分析的话还可以使用如Metabase这类开源方案。
Python Jupyter Notebook
我用这个绘制一些数据分析的图,比如这种:
源代码见 index_month_quote_change.ipynb。
其他工具
Excel
用Excel画图不是不可以,你可以用数据绘制图,也可以用单元格来绘制马赛克的图,不过我一般用它来绘制投资组合的一些走势图和状态图。
配图存储与展示
图片我一般用VSCode插件 markdown image paste 配置AWS S3后,可将复制到剪贴板的图片一键上传至S3桶,之后可通过桶绑定的域名访问图片,本博客所有的配图都是如此存储与展示的。具体详见《我的笔记系统》VSCode插件一键存储图片至S3节。
关于绘图工具的思考
没有一种工具是万能的,万能如Excalidraw也无法替代Lucidchart/Draw.io/Google Drawings/Visio这些复杂的工具(他们更适合复杂的对精确性有高度要求的图)。更没有一种软件工具能替代纸和笔。工具更重要的是背后使用它的人,能用图片讲好一个故事才是我们需要不断提升的能力,一个工具能做到不限制你的发挥就算适合的好的工具。
进一步阅读
技术文章配图指南
技术配图的一些心得
更新时间: 35个月前 版本: 8171f612c
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
人生游戏之路
24 January 2017 21:52 | 4814 字 | 12989 次阅读
个人成长
知识管理 • 知识体系 • 终身学习 • 快速学习 • 元学习 • 个人品牌
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
前段时间很火的一个美剧《西部世界》,剧中的机器人和正常的人类很相似,其中“老鸨”机器人甚至可以通过修改自己的参数使其变的更为强大。在我们的想象中机器人应该是有很多参数去控制他的行为和“意识”,这好比在游戏中的角色可以通过打怪升级提升自己技能点,而现实世界中一个人也可以通过不断学习与历练提高自己的思维能力和专业技能。
那么问题来了,普通人如何通过后期训练系统化的提升自己的综合能力?要直接回答这个问题是很困难的,因为没有标准答案,每个人的情况都不同,所遇的环境也是大不相同,但是我认为依旧可以通过后期自我训练达到一定的目的。
想象一下,我们和游戏角色其实是很相似的。以Dota为例,Dota的一场比赛目标很明确,就是推塔打掉对方基地。在这个过程中你得先通过不断的杀小兵去获取经验值和金币,而经验值可以去获得最高五个技能属性,金币可以买适合自己的装备。如果要玩好一个角色,需要我们了解角色的属性,到底在团队中处于什么定位,角色本身需要什么样的升级节奏,然后配合同伴去赢得一场场局部战争,最终赢取整个游戏的胜利。而在现实世界中,我们从一出生就不断的需要在人生游戏中赢取胜利,否则就会被社会“淘汰”,每个人有不同的优势劣势,通过发挥自己的长处获得适合自己的专业能力,使用专业能力不断工作赚钱,钱又可以使自己获得更好的生活条件,整个社会是分工协作的,所以我们还需要和合作伙伴们一块完成不同的挑战任务,最终去实现我们自己的目标。
Dota游戏的复杂度没法和人生游戏的复杂度比,为了简化整个过程,我们可以从这几个角度去分析如何使整个人生游戏变得更清晰简单。首先最重要的是目标,一个人没有目标那和咸鱼有什么区别。然后是时间,时间是非常重要的,是之后我们其他能力和资源的来源,世界的公平很大部分体现在每个人的时间都是相差不大的。有了时间后资产也很重要,这里的资产包括资金和知识,知识是无形资产,在一定条件下可以等价为资金。之后是技能,社会高度分工带来的是每个人都需要社会所需的专业技能,专业技能从后天习得,但是也和先天有关,选择适合自己的技能是一个很重要的点,跟随社会趋势找准自己的定位很关键。最后是社交,社交就是与别的人建立连接,所以需要建立自己的品牌,积累自己的信用,这样别人才会乐意和你合作。
从这几方面出发可以不断的延展开来,以下是我按照这几个方面绘制符合我自身特点的脑图:
目标管理
人要有目标可能是一件很简单也很难的事情,有的人在小时候就能建立自己的目标并不断的去实现它,这种人一般都是与众不同的人,在社会里大家一般称之为“伟人”。大部分的人对于自己的目标都很模糊,尤其是我们的社会目前笼罩着浮躁的气氛,社会还处于只以赚钱多少为准来衡量一个人的成功与否,如果不能免俗,那以赚一定数量的钱作为自己的目标也是可以的,但是如果你连自己的目标都搞不定的话,那这后面的都不需要再看了。
目标可以是短期的也可以是长期的,举例来说,你可以以财务自由来作为自己的长期目标,但是这个目标不能这么模糊,否则是铁定失败的。一个好目标是自我描述的,带有一定的期限,能不断拆分成小目标并落地在单个动作。那这个财务自由的目标可以优化为:我要在5年内赚够100万。这里有个问题就是100万能不能让你财务自由,所以需要你分析自己的资金缺口情况,如果自己搞不清楚的话,可以找专业人士做个理财规划去梳理下自己的财务目标。目标同时也不能超出自己潜在能力的最大范围,你要定个首富的小目标一个亿的话,那也大概率失败。
目标可以定多个,按照不同分类和期限来制定,但是一定不能超过五个,否则很难去管理跟踪。
时间管理
时间就是金钱
“时间就是金钱”,重要的话心中默念三遍!!!要理解时间的重要性也是一件很难的事情,我在很长的时间里是不理解这个概念的,也有不少人并不知道这句话的真实意思。这句话并不是说“时间==金钱”,时间转化成金钱需要一个中间过程,这个过程取决于很多要素,包括知识技能及跟随趋势的能力等。
时间如何变钱?我们上班工作就是出售自己的时间给公司,公司支付给我们一定的报酬,这是最常见的主动转换形式。其他一些形式比如创作、中介、广告、套利、投资等。
时间杠杆
站在每个人使用时间的角度去看,它是动态的。比如阿丑每天在重复性做一些整理资料如去某个地方查询下载保存整理,阿莫完成同样的任务只需要编写一个定时脚本让计算机自动帮他去做,只需要花十分之一的时间,剩下的十分之九可以做其他的事情了。阿莫在使用工具提升自己的“时间杠杆”。
如何产生更多的“时间杠杆”?通过使用工具来提升效率,当工作量一定的情况下,工具的效率越高你的“时间杠杆”也会越大,所需要完成同等工作量的时间也越短,也就是你可以在一天内做比别人更多的事情。如使用正版软件可以为你节省时间,毕竟你无须去浪费时间找破解资源;使用付费网盘去存储你的重要资料,比你自己维护一个私有网盘要更靠谱,也无需耗费时间去维护它;购买质量好但是更贵的物品也会长期节省你的时间。另一种方式是让专业的人做专业的事,你只需要付费让他解决这个你无法解决或者耗费大量时间的问题。
时间黑洞
没有被管理好的时间就是“虚无”,很多不良行为都是“时间黑洞”,如无休止的重复玩游戏、刷剧、做围观群众、为无意义的事情站队争论等。还有一些事情比较隐晦:每天上班需要很长时间在公交中挤过;用盗版破解软件并为此花费很多时间;大量时间被浪费在重复寻找资料然后对比最终遗忘(很大概率是没有管理好自己的知识资源)。
如何降低“时间黑洞”的消耗?时间的消耗是很容易被忽视的事情,不少人会耗费大量时间去做无效的事情。一个视频网站,我去年曾耗费两百多小时去看一些没什么价值的视频,但是我本身并没有意识到这个事情直到视频网站出来年度账单。为什么没有意识到?因为每天可能耗费很少的时间,长期累积下来就是一个可怕的浪费,两百小时完全可以做很多更有价值的事情,而我却为视频网站的流量贡献出了自己更重要的时间,如果转换成资金也是一笔不小的支出。如果要我直接为视频网站出这么多钱,我肯定是肉疼的,但是支付时间我却没什么痛苦,实际两者转换后是等价的,只是大脑对钱更敏感。
管理时间
“时间杠杆”可以开源我们的时间,开源的同时也要节流,要节流时间就需要我们记录统计自己在每类事情上分配了多少时间。同样的一天,阿丑刷剧花了2小时,而阿莫花了0.5小时,那“时间黑洞”消耗了阿丑更多的时间,阿莫可用变钱的时间就比阿丑多。
要解决“时间黑洞”的问题,首先我们要学会统计自己的时间,就像记账一样,让大脑对时间就像钱一样敏感,直至形成一个习惯。
我现在所用的时间管理App为“OmniFocus+Due+时间块+日历”:
这个组合里OmniFocus是管理长期计划的,它会把我的目标拆分成多个可执行的动作,然后按计划定时提醒我去完成;
Due这个软件会重复提醒某个事情直至我关闭它,这个软件可以帮助我形成一些习惯,如每小时喝水、每小时站立、每晚阅读一小时、每天锻炼一小时等;
时间块这个软件可以让我记录我在某类事情上耗费的时间,比如工作、睡觉、交通、兴趣爱好、个人项目等分配的事情,有了数据的支持,我会更容易知道自己有没有把更多的时间分配给更重要的目标上来;
日历可以设置某些今日必须要完成的事情,任何截止今日必须完成的事情都可以用日历来提醒,它可以帮助我回顾每天的事情有没有完成,有哪些事情需要延迟完成。这套工具链可以帮我更好的管理时间。不过能否对你起作用取决于你的执行力,工具只是辅助作用,在形成一定习惯后就无需这么多工具了。
资产管理
如何财务自由?
在把时间转变为钱后,我们可以用钱来购买自己的资产,从金融的角度看:
公式一:月现金流=月收入总和-月支出总和
公式二:财务自由=被动收入>月支出总和
请记住这两个朴素的公式,重要的话说三遍!!!
公式一告诉大家,只看收入不看支出都是耍流氓,哪怕你月收入十万,月支出只要高于你月收入,你还是一个穷光蛋,随时有破产的风险,月现金流越高越好,有了很好的现金储备,你才能有去做买买买的剁手行为或投资各类资产的能力。提高月现金流有两个方法,当然是提高收入,降低支出了。收入的提高依赖于你的主动收入和被动收入,这里面很多人更看重主动收入,也就是工资收入,但是真正能让你财务自由的却是被动收入,所以想办法提高被动收入才是你重点要考虑的。不过在前期一般大家都属于成长期,并没有被动收入能力,这就像玩Dota一样,大后期一般前期都废的很,被动技能一般都是后期获得,前期主要是保命成长。所以工资在前期还是很重要甚至可能是你唯一的收入来源,吃饭的技能还是得储备几个,手中有粮心中不慌。
被动收入的来源一般有创作、中介、广告、套利、投资等渠道,如何建立这些被动收入渠道呢?还是以阿丑为例,阿丑会写文章,立志做个金融自媒体人,经过一年多的耕耘,在阿丑码了二十多万字后,关注粉丝终于到十几万了,每天打赏阿丑的粉丝也不少,阿丑初步获得了创作的被动收入渠道;有个金融平台需要阿丑的流量来给他们平台新增用户,阿丑会定期给这个平台通过软文导流用户,平台给阿丑一定比例的中介分成,阿丑获得了中介分成被动收入渠道;阿丑偶尔也会给一些品牌在文章底部做一做广告,阿丑获得了广告被动收入渠道;业余时间阿丑也研究投资,市场好的时候阿丑也是可以赚钱的,再偶尔玩个信用卡套利,被动收入逐渐比他工资收入都高了,这个时候阿丑下一个目标或许就是创业了。
财务自由和总支出关系也很大,总支出其实反映了一个人的欲望,如果欲望无止境,总支出的增长肯定是比被动收入高的,这样永远也实现不了财务自由。在收入增长的前提下,支出增长也是很有必要的,但是过度增长只能反映出你无法管理自我,甚至会被本我吞噬掉。
系好你的保险带!
老司机发车前肯定要系保险带的。作为一个家庭,拿出5-10%去买商业保险是一笔很划算的买卖,保险真正起的作用是一个杠杆账户,能以很多倍杠杆转移你的资产风险。
技能管理
前面说了在成长期里大家唯一收入来源大多是工资,而工资又来源于你能以什么价格出卖你的技能。技能是你时间和资金转换的桥梁,技能越稀缺转换效率也越高,同等时间下收入也越高。所以掌握一些社会趋势需要的稀缺技能是一项值得研究的事情。
知识与思维
一个人学习新技能的速度取决于他管理知识的方法,一个人能否适应社会革新取决于他的思维模式。
知识的管理我用OneNote来辅助处理,只需要记住它存放的位置,通过好的分类记录,知识是可以成一个体系自动去建立连接。想进一步了解可看我的这篇文章《用OneNote管理你的知识》。
技能树
技能树的难点在于跨界组合,掌握一类知识并不难,难的是如何跳出局部去抽象知识框架,然后通过这个框架去快速学习另一领域的知识,如果能建立两种领域以上的知识,那学习新领域的速度一定会很快,因为你可以站在另外一个维度去看待这个领域的问题,解决问题的方式也会多元化起来。更重要的是眼界的提升,不会因为自己的角色而限制自己独立思考。想进一步了解可看我的这篇文章《如何快速学习一项新技能?》。
社交管理
信用
未来是一个信用社会,很多趋势可以看出来社会已经由传统的熟人关系型往契约型发展,契约型社会需要了解每个人的信用值来决定是否给这个人某些权益。英剧《黑镜》第三季第一集中描述了一个未来信用社会,人与人之间的评价决定了另外一个人的社会地位,信用分把人分为不同阶层,你的分数高,你所拥有的权益也更高,享受更多的社会福利。所以维护好自己的信用历史很重要,不要留下污点,否则很难抹掉的。
品牌
个人品牌未来会越来越重要的,建立自己的个人品牌,意味着可以让更多的人信任你并与你合作,机会自然也多。
如何建立个人品牌?一般可以通过关注一个领域并在这个领域有深的研究,不断的通过各大社区平台分享交流,好的内容自然会通过社交传播出去被更多的人知道,你的品牌影响力也会逐渐的积累。真诚一些,少点套路,兴许是个不错的开头。
BMPIGitHubTwitter学习技术投资|English
打造优雅高效的写作环境
26 June 2016 21:39 | 4763 字 | 10214 次阅读
技术写作
写作 • Hexo • GitHub Pages • 图床
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
写作是一件很有趣的事情,长期形成良好的写作习惯可以帮助我们有条理的处理解决问题,同时给人生留下点回忆也不错。在我们很小的时候也许就有记日记的习惯,大脑的记忆量毕竟很有限,所以我们用纸和笔来记东西,但是传统的纸和笔存在很大的问题,无法长久的保存东西。进入互联网时代后我们有了很多选择,各类网站博客、专栏、微博、公众号等可以让我们写作的地方,但是这些都无法解决以下几个问题。
排版。编辑器不支持Markdown语法。每次看到那种糟糕的排版都让人没有看的欲望了。很多宝贵的时间都浪费到排版上了。
版本管理。如果有多个版本常规的博客只能你自己来保存了,一旦出错就很难回到之前的版本了。
难以长期保存。诸如网站关闭或者电脑损坏丢失都会导致之前的资料丢失,资料很难长期保存。
网站难看。很多网站博客只提供一些难看的主题,你很难定制好看的主题,大部分这些难看的主题对移动终端还不友好。
文章发布流程繁琐。很多网站的编辑写作界面都很糟糕,发布起来还很麻烦,如果能通过简单的命令就可以发布上去是多么优雅的事情。
不支持自定义域名。难以个性化的域名很难体现个人的品牌。
受限的写作环境。一些网站为了通过政府部门的内容审查,你写的内容很可能不经意被强制删除,总之在别人的地盘上你的东西不一定是你的。 以上就是可能存在的几个问题,我理想的写作环境应该是可以解决上述几个问题的,下面我通过几步来打造一个专属于我们自己的优雅的写作发布环境。
环境搭建
博客系统(Hexo) &主题(Next)
首先我们需要一个博客系统,目前市场上存在很多完整的博客系统:如Wordpress或一些CMS系统等,但是这些系统都太大,很臃肿。我们需要一个轻量级别的博客系统,并且能配合GitHub Pages来管理我们的文章及相关信息。Hexo就是一个满足我们需求的博客系统。 在Mac下通过命令行安装:
npm install hexo-cli -g
hexo init blog
cd blog
npm install
hexo server
如果是Windows或者Linux系统也可以通过如下命令来安装,Windows系统需要提前安装好NPM软件包,安装流程可以通过Google或者官方文档来查看。
这时候你已经部署好了Hexo博客系统,并且初始化好了博客系统所需的文件,blog就是你的博客根目录。这时候我们需要安装一款名为Next的主题,这个主题也是本博客目前使用的主题,你也可以选择你自己喜欢的主题。Next主题很强大,响应式设计并且支持很多第三方服务的特性,这个大家可以通过官网Hexo来查看。
经过Next主题官方文档的指导后,相信你已经配置好了该主题。那目前我们已经完成了博客系统和主题的配置,接下来需要把博客系统发布到互联网上,这样所有人就可以来访问我们的博客了。
系统部署(GitHub Pages)
如果是传统的个人博客的话,一般需要租一台在线的VPS系统或者空间来存放我们的软件,但是这样你需要操心很多事情了,不仅需要自己维护服务器,还需要每月付费供养服务器,比较麻烦。我们有更好更优雅的方式,就是通过GitHub Pages来存放我们的博客系统。具体安装介绍可以看官网GitHub Pages。
为什么我们需要GitHub Pages来部署我们的系统,一方面是因为GitHub Pages是免费的,很多服务器的维护都不需要你操心,更重要的一方面是你可以用Git来管理你的博客,通过几条简单的命令就可以发布、回滚之前版本来管理你的博客网站。
git add –all
~\(git commit -m "Initial commit"
~\)
git push -u origin master
上面的命令就是在你的blog目录里面操作的,一旦你注册好了一个GitHub帐号后,在上面建立一个仓库(repository)后,在你计算上设置好SSH Key来打通和GitHub仓库的通道后,你就可以通过上述几条命令来把你本地的代码上传同步到仓库里面去。用人话讲就是,你通过那几条命令把你的网站上传到GitHub的网站上去了。然后你可以访问: http://username.github.io ,这网址就是你的博客地址,需要把里面的username替换为你的GitHub账户名。
域名绑定
毕竟 http://username.github.io 这种域名无法体现我们对逼格,可以在Godaddy或者万网注册一个域名去,可以以你的名字来注册,一般短的都注册不到了。万网的域名如果托管到阿里云到话需要备案,至于备案流程的话阿里云会有相关的流程帮你做,大概需要半个月之久。备案后我们可以在域名管理里把域名解析到 http://username.github.io ,如果觉得万网的DNS不好的话可以把DNS解析放到DNSNode上面去,然后在里面设置CNAME子域名。
然后在blog目录里面的source目录下新建一个CNAME文件,文件内容就是你自己的域名。
这样你可以直接访问你的域名就可以访问你的博客了。实际上做了一次跳转。
运营统计
接下来我们需要对博客和主题进行一些配置,以达到更好的一些效果,比如评论分享(多说)、文章阅读次数(LeanCloud)、文章分类标签、侧边栏的设置、第三方服务的配置(Google Webmaster & Analytics、腾讯站点分析)。这方面可以看Next主题的配置文档。 大概说下流程,评论可以在Next主题里面启用多说评论系统(你需要注册一个多说账户),文章阅读次数可以参考这篇文章。文章分类标签可以在用Hexo命令来新建分类和标签页面,这个可以参考Hexo官方文档,如果你想对主题CSS等文件进行一些修改后发现并没有效果的话,那可以先清空Hexo数据库和产生的文件。进入blog目录在命令行执行:
hexo clean 清空Hexo数据库和产生的文件
hexo d -g 重新产生博客文章并发布到GitHub,前提得在Hexo里配置好GitHub账户
侧边栏得配置可以在Next主题里面配置。第三方服务等都可以参考Next主题的配置文档。这样通过Google的Webmaster你可以把你的站点设置的对搜索引擎更友好,这样别人就可以方便的通过搜索引擎找到你的文章了,提高流量了。同时设置腾讯分析和Google Analytics可以让你很方便的了解网站的流量和访问情况等。腾讯分析可以每天或每周订阅网站统计分析报告。
写作发布
搞定了烦人的环境配置后,我们开始了最重要的部分:写作!一切都是为了更优雅的写作。
Markdown语法
Hexo博客系统的写作方式很简单,通过Markdown语法来写作。首先让我们新建一个文章吧。
hexo new title title是你的写作
hexo d -g 重新产生博客文章并发布到GitHub,前提得在Hexo里配置好GitHub账户
这样在blog/source/_post目录就产生一个title的md文件,这个文件就是我们要写作的,当然有个小麻烦在于你每次敲完命令都需要去找这个文件,我们可以通过以下的设置来让hexo系统自动用软件来给我们打开这个要写作的文件。首先在blog目前新建一个scripts的目录,然后在里面新建一个open-tools.js的文件,文件内容如下:
var exec = require(‘child_process’).exec;
// Hexo 3
hexo.on(’new’, function(data){
exec(‘open -a “/Applications/Mou.app” ’ + data.path);
});
这个文件的意思就是新建一篇文章后hexo会自动用Mou这个app来打开这个md文件,然后你就可以用Mou这个Markdown编辑软件来写作了。 至于Markdown语法怎么搞,网上有很多教程,但是为了直观的给你一个感受,你可以看看我这篇文章的md文件,然后对比下输出的文章就大概理解Markdown的精髓了。
编辑工具
目前我用Ulysses来书写Markdown文件。
写作发布流程
在Mac打开iTerm终端
cd blog
新建文章并用Mou编写文章内容
hexo new title
发布文章到GitHub Pages
hexo d -g
修改文章后继续执行第3步即可重新发布上去
版本管理
如果我们想回到某一个写作版本的话也可以很方便的通过GitHub仓库里面进行回滚操作。每次当我们执行hexo d -g命令的时候实际上就是产生一个版本,你可以登陆到GitHub仓库里面看到每次提交的版本,当然这个仓库存放的是已经产生的Html文件,也就是在blog目录里面的public目录,这个目录的所有文件都会被发布到GitHub仓库里面去。但是source/_post目录里面存放的才是你真正的源文件,都是些md文件,那这些文件并没有被GitHub仓库管理,所以为了对这些源文件进行版本管理,你需要在GitHub新建一个仓库用来存放blog目录,blog目录里面有些文件并不需要发布到GitHub,这些不需要的文件你可以在.gitignore(存放在blog根目录下,如果没有你可以新建一个)里面写明,我的这个文件内容如下:
.DS_Store
Thumbs.db
db.json
.log
node_modules/
public/
.deploy/
这里面的node_modules目录里面放着一些node的模块,public目录存放着hexo产生的html文件(也就是你的博客)。当你部署好这个仓库后,你可以通过以下git命令来把你的博客系统发布GitHub仓库:
git add . 把此次更改写入git缓存
git commit -m “some changes” 把此次更改提交到本地库里去
git push 把本地库更改同步到GitHub远程库里去
git status 查看本地git库状态
在这个库里你可以找到你写作源文件的所有发布的版本,当然也可以回滚到任意版本中去,相当简单方便。
图床
当我们写作的时候,经常需要引用图片到文章里去,如果图片少的话我们可以直接把图片放入每篇文章的目录里面,然后发布到GitHub里去,但是这样会导致GitHub仓库变得越来越大,并且同步会非常慢。所以我们需要一个稳定强大长久的私人图床。一般小型网站的图床都不做考虑。
新浪微博图床
这个可以安装一个Chrome插件来实现(微博图床)。
微博图床的优点就是自动给你产生不同尺寸的图片,还贴心的给你Markdown的语法,但是一个问题可能出在新浪微博屏蔽这种外链了,长期会有一些隐患的。
七牛云存储
付费的存储空间里面七牛云存储稳定而且不错,免费认证的用户就有10GB存储空间,每月10G下载流量,足够博客用了。当你注册后建立一个新空间后,下载一个qrsync同步工具,可以很方便的把你本地的文件推送到七牛云存储空间里面,具体安装文档你可以看官方的说明文档,我这里贴出来这个工具的配置文档(conf.json)供你参考:
{
“src”:“your src dir”,
“dest”:“qiniu:access_key=123&secret_key=456&bucket=789”,
“debug_level”: 1
}
在这里把src替换成泥实际目录的绝对路径。dest里面替换access_key、secret_key及bucket。 这样每当你在src目录里添加图片后,执行如下命令即可把src目录的文件推送到bucket里面。
qrsync conf.json
需要注意的是src目录里面不需要建立子目录,因为这个同步工具会忽略子目录,直接把文件全部放到src根目录即可。 那怎么访问bucket里面的图片呢?每个存储空间都有一个域名,那任何一张图片访问的形式就是:http://七牛存储域名/xxx.jpg 。那么所有图片都可以这么调用外链。考虑到存储空间有流量的限制,为了防止别人调用你的图片,你可以在存储空间管理里面设置白名单,把你的域名添加进去即可,这样只有你的域名可以访问这个图片等外链了。 如果你觉得七牛云存储无法长期存活的话,你可以把本地这个图片文件夹同步到OneDrive网盘里去,你甚至可以把所有的文件都放到OneDrive里面同步上去,这样几个地方互为备份,就可以长久的写下去了。
优雅的写作环境
这就是我觉得优雅的写作环境了,任何不懂编程的人也可以这么给自己搞一个长期写作的地方,你只需要花几十块钱买一个域名,剩下的都是免费的。除了环境搭建前期稍微有点复杂外,一旦环境搭建好后剩下的工作就很简单。 你拥有了一个非常简单强大的写作环境,流程简单,只需要输入两条命令+写一个MD文档,不需要麻烦的排版,也不需要麻烦的发布,图片等存储也是非常简单的,只需要一条同步命令即可在MD文档里访问图片地址。然后你就可以在任何地方访问你的博客了,各类文档都有几个地方互为备份(异地灾备),长期下去你写的东西都可以安全的存储、访问和搜索,同时有强大的版本管理,任何时候你都可以回滚到任意版本,不要担心出错。
优雅的写作是为了更自由的表达自己。
更新时间: 29个月前 版本: 6c7670696
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
善用GitHub
写在第二十五万字
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
我与ChatGPT结对编程的体验
21 May 2023 21:5 | 4640 字 | 22524 次阅读
chatGPT • Prompt Engineering • Pair Programming
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
在软件开发过程中,结对编程(Pair Programming)是提升代码质量的一种有效方式。在之前的文章Google软件工程之文化篇打造团队知识共享文化中,我曾对这种编程方式做过简单的介绍。
结对编程有两种模式:乒乓模式(Ping-Pong)与驾驶员观察者模式(Driver-Observer),前者适合以TDD的方式开发,后者适合老带新。本文主要采用后者的方式,我以一个观察员的身份去规划系统功能设计并审查代码,然后使用一些Prompt Engineering的技巧让ChatGPT以驾驶员的身份去开发实现。
Prompt Engineering相关的课程推荐观看由Andrew Ng与Isa Fulford讲授的ChatGPT Prompt Engineering for Developers免费短课程,本文也会将课程中的一些Prompt Engineering技巧应用到此次的实际开发中。
这次结对编程的目标是实现一个群发消息给用户的功能,项目背景是一个Slack工作区有大约几千个用户,管理员希望能够通过某种方式以群发的方式向这些用户发送消息,而不是一个一个地去手动发送。但客户也提到无需对此功能进行过多的开发投入,甚至不需要一个管理界面,只需要有方法能让管理员去群发消息即可,哪怕直接操作数据库都可以。
这个功能看起来很简单,就是要实现一个群发消息的功能,最简单的就是在一个大循环里面去遍历所有用户,然后调用发送消息的接口去发送消息即可。但这样的实现方式有几个问题:
如何触发消息发送的动作?调用某个接口?还是直接操作数据库?
消息怎么存储?存储在数据库?还是存储在文件系统?
消息如何定制化?是否需要支持消息模板?
消息发送的频率如何控制?是否需要支持消息发送的时间间隔?
消息发送的状态如何跟踪?是否需要支持消息发送的状态查询?
消息发送失败如何处理?是否需要支持消息发送失败的重试?
这些问题看起来都不是很复杂,但如果要实现一个完整的群发消息的功能,就需要考虑到这些问题,而且这些问题都是需要在设计阶段就要考虑到的,否则后续的开发就会变得很被动,甚至会导致重构。
目前ChatGPT因为有输入上下文大小的限制(3.5的Token最大数量是4K,4的话最高能到32K),无法把整个项目的背景都输入进去,这些限制会影响我们使用它去开发一个完整的功能,所以我们需要把这些问题分解成一些小的问题,然后使用ChatGPT去解决这些小问题,最后再把这些小问题组合起来,就可以实现一个完整的功能了。
现在,是时候与ChatGPT开始结对编程了,我的Prompt都是英文的,当然也可以用中文,不过感觉英文的Prompt效果会更好一些,在写英文Prompt的时候也无需考虑语法是否正确,中式英文也是可以的,因为ChatGPT对语言的理解能力是很强的。另外由于篇幅限制,本文会将ChatGPT的回复以链接的形式展示,读者可以点击链接查看ChatGPT的回复。
Prompt: Let’s start to implement this feature, use cloudflare worker with typescript, there is a database to store message sent history, it is like the sql migration behavior, there is a dir to store the message template files, the message send will store in to the database and will not send again. So we will have a sendMessage function, this function will first to scan the message dir to get the file list and will compare the database to get the not send message files but every time it only to get one un-send message templat then use the slack post message to send the message to all the users. That means every time the sendMessage function execute it just send one message to all users if there is. the message template file is a json like file but store the slack blocks template, the template can use the data to render a real slack message blocks.
Based this, you can implement one step by step.Rember I have the project and no need to give me how to init the project, just start the function implement and the template design, I also want to know how to build the blocks from the template, and there is also a sqlite database to store the users table, so you may need to create a migration sql to create the new message table.
Answer: From ChatGPT.
这里的Prompt是一个比较长的描述,所以可以提出了第一个Prompt的技巧:撰写明确而具体的指示。这个Prompt的目的是给予ChatGPT要实现功能的背景及功能的大致实现思路,这样ChatGPT就可以根据这个背景去实现功能了。
在第二段中,我还要求它给予详细的实现步骤,并且不需要考虑初始化项目的步骤,因为这不是一个新项目,我们需要在已有代码的基础上去实现新的功能。所以这里的Prompt还有一个技巧:给模型思考的时间。Step by step是一个具有魔力的Prompt词汇,可以让ChatGPT花更多的时间去计算思考这个问题,而不是直接给出一个答案。
这个Prompt的意图有以下几个:
让ChatGPT以Cloudflare Worker + Typescript的方式去实现这个功能;
有一个数据库用于存储消息发送的历史记录,类似于SQL的迁移行为,这里的数据库是SQLite;
有一个目录用于存储消息模板文件,消息发送时会从这个目录中读取消息模板文件;
消息发送时会把消息存储到数据库中,以便后续查询;
消息发送时会把消息发送给所有用户;
消息模板文件是一个JSON文件,里面存储了Slack的消息模板,可以使用数据渲染出一个真实的Slack消息;
每次发送消息时,只会发送一条消息给所有用户;
根据以上这些要求,让ChatGPT一步步去实现这个功能,并且不需要考虑初始化项目的步骤;
同时让ChatGPT创建一个数据库迁移SQL,用于创建消息表。
但在ChatGPT的回答中,我发现它错误理解了我的一个需求,于是我继续在这个会话中给它提出了一个新的Prompt:
Prompt: I think there is some mistake: - The getUnsentMessages function need to scan the messages dir and compare the database which just save the sent message and only get the oldest unsend message file. - the renderMessageTemplate function use the template and a dic to render a real json, u need to think how to render the template, is there any tmeplate render methond in typescript.
Answer: From ChatGPT.
在这一轮的对话中,ChatGPT正确的理解了我的需求,于是我把它的代码整理了后合并入了项目中,这样这个功能的大致框架就完成了,接下来就是一些细节的问题了。这里可以提出第三个Prompt的技巧:迭代Prompt。如果你发现ChatGPT没有正确的理解你的需求,那么你可以继续在这个会话中微调你的Prompt,直到ChatGPT正确的理解你的需求为止。
后续的Prompt我一直在与ChatGPT讨论如何使用Typescript的模板引擎去渲染消息模板,但是ChatGPT一直没有给出一个满意的答案(见这个及后续的几个Prompt),于是我决定放弃这个方案,改用另外一种方案,这里可以提出第四个Prompt的技巧:放弃方案。如果你发现ChatGPT没有给出一个满意的答案,那么你可以放弃这个方案,直接开启新的会话探索另外一种方案。
然后到了数据库的设计,我给ChatGPT提出了一个Prompt:
Prompt: I want to implement a function, it get the cloudflare R2 bucket directory to get the message file list, and then querey the database table to know if this file are sent to all user, that means there is a status filed to record if the message file is sent to all user, becasue the users size are big, so it will need to sent mult times, can u design the table and give the migration sql, the database is sqlite, currently it has a Users table, so you need to consider how to record the message file if it is send to all users.
Answer: From ChatGPT.
在几轮Prompt迭代后,数据库的设计也完成了。不过在这个过程中,因为它给的代码不完整,于是我又给它提出了一个Prompt:
Prompt: // first we need to iterate through the list of files and get the file name list, then query the database table MessageBroadcast to insert the file name which is not in the table for (const file of getMessageFileList.objects) { const filename = file.key.split(’/’).pop(); const messageBroadcast = await c.env.DB.prepare(select * from MessageBroadcast where message_file_name = ?
).bind(filename).first(); if (!messageBroadcast) { await c.env.DB.prepare(insert into MessageBroadcast (message_file_name, send_status) values (?, ?)
).bind(filename, false).run(); } } regarding of the sql, how can I give the file name list to insert those files are not in the table by one sql statement?
Answer: From ChatGPT.
在这轮对话中,我们又可以提出第五个Prompt的技巧:给出代码片段。如果你发现ChatGPT给出的代码不完整,那么你可以给出一个代码片段,让ChatGPT帮你完成这个代码片段。
然后我发现它的代码我难以理解,于是我又给它提出了一个Prompt:
Prompt: the sql seems magic, can u give the explain or some examples to show how it works?
Answer: From ChatGPT.
在这轮对话中,我们又可以提出第六个Prompt的技巧:代码解释。如果你发现ChatGPT给出的代码你难以理解,那么你可以要求它给予完整的解释,或者用更简单的方式重写它的代码。这个过程中,还可以提升自己的技术能力,这也是结对编程的一个好处。
在与ChatGPT的这轮对话中,关于数据库SQL的实现时,有个需求是根据数据动态生成SQL,但是ChatGPT并没有给出一个满意的答案,于是我又给它提出了一个Prompt:
Prompt: this generate: INSERT INTO MessageBroadcast (message_filename, status) SELECT filename, ‘UNSENT’ FROM (VALUES (‘1681390809.mustache’)) AS files(filename) WHERE filename NOT IN (SELECT message_filename FROM MessageBroadcast); But I want to generate the: INSERT INTO MessageBroadcast (message_filename, status) SELECT filename, ‘UNSENT’ FROM ( SELECT ‘filename1’ AS filename UNION SELECT ‘filename2’ AS filename UNION SELECT ‘filename3’ AS filename UNION SELECT ‘filename4’ AS filename ) AS files_to_insert WHERE filename NOT IN ( SELECT message_filename FROM MessageBroadcast );
Answer: From ChatGPT.
在这个Prompt中,我用到了第七个Prompt的技巧:few-shot。这个Prompt的技巧可大有来头,因为这是GPT-3的论文Language Models are Few-Shot Learners标题的由来。如果你发现ChatGPT没有给出一个满意的答案,那么你可以给出一个更具体的例子,让ChatGPT帮你完成这个例子。
接下来是考虑到SQL的性能问题,因为这是一个大小表联合查询的问题,可能会存在性能的问题,我想与ChatGPT进一步讨论下,看是否存在优化的空间,于是我又给它提出了一个Prompt:
Prompt: Think step by step, the question context is, the Users table have almost a thousand users, and the MessageBroadcast table have a record means a message need to send to all users, and the UserMessageBroadcastStatus is like a middle table to record if this message has sent to all users, so the requirment is we need to find a batch size like 30 users who are not sent to this message, so what is the sql like?
Answer: From ChatGPT.
这里依旧用到了Prompt的第二个技巧:Think step by step。但它显然没有给出一个满意的答案,于是我又给它提出了一个Prompt:
Prompt: the question is at the beginning, the message is created in the MessageBroadcast, and the UserMessageBroadcastStatus has nothing, it only will insert data after the message is sent to the user, then a process will updat the table, think again!
Answer: From ChatGPT.
这显然也用到了Prompt的第三个技巧:Think again,最终它给了我一个满意的答案。
这些对话的过程中,还用到了以下的一些Prompt技巧:
要求结构化输出:比如直接要求它输出SQL语句,或绘制架构图:通过Prompt提供系统组件的关联关系,然后让ChatGPT以Mermaid格式输出时序图(Sequence Diagram)或C4图(C4 Diagrams);
翻译代码:比如将Python代码翻译成Typescript代码;
总结:比如总结一下这个问题的解决方案;
这些Prompt技巧,都是我在与ChatGPT的对话中,不断尝试的结果。最终在20多个Prompt的对话中,我与ChatGPT结对编程耗时不到两小时,完成了这个功能的开发,这是一个非常高效的过程。
在上面介绍了如何与ChatGPT以驾驶员观察者模式的方式结对编程,当然也可以以乒乓模式的方式结对编程,比如在这个对话里给ChatGPT提供一段函数代码,然后让它写单元测试。这个过程也可以反着来,这样就达到类似于乒乓模式的结对编程了。
在结对编程中,编写代码只是一部分工作,代码之外,ChatGPT还可以帮助编写文档,甚至参与架构设计的讨论。比如在这个对话中,我把系统架构图的Mermaid格式的文本给了ChatGPT,让它给我一些系统架构方面的建议,通过这种方式,ChatGPT甚至能介入到系统设计的讨论中,并给我可能具备一定价值的参考意见。
需要提醒的是,因为ChatGPT的局限性,目前还不能给予我们非常完美的回复,在每次的回复中,尤其是涉及到一些复杂的细节,或者是非常新的它不知道的信息,它就有可能给出一些捏造的答案。比如在某些代码中参杂了错误的语法或接口调用,这时候我们就需要自己去判断,这也是目前ChatGPT无法替代人工编程的原因之一。因为这个局限性,这也是我在本文中优先采用驾驶员观察者模式的原因,因为这样可以让我们更好地控制ChatGPT的输出,从而更好地控制我们的开发过程。
最后,在本文中提到的各种Prompt技巧,其实都是在尝试规避它的局限性,比如通过输入清晰明确的指示来规避它的随意性,通过总结来规避它输入上下文长度的限制,通过迭代Prompt来微调它的输出,通过few-shot让它更好的理解我们的意图。希望这些技巧能帮助你更好地与ChatGPT结对编程。
更新时间: 25个月前 版本: 2ddf2ecde
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
AI驱动开发:从Prompt到Product(直播)
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
ChatGPT背后的语言模型简史
7 May 2023 48:48 | 10737 字 | 21782 次阅读
深度学习
chatGPT • NLP • 神经网络 • 深度学习 • 大语言模型
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
本文修订记录
2023/05/10更新未来展望部分;
2023/05/08感谢张浩在路上对本文的审阅和修改;
2023/05/07与ChatGPT合作完成初稿;
2023/04/22选题与大纲设计;
自然语言
人类文明的高科技
人工智能的一颗明珠
从神经网络到深度学习
神经网络
深度学习
从文本到词向量
循环处理文本序列
从序列到序列
注意力是全部
大语言模型时代
生成式预训练
大力出奇迹
未来展望
进阶阅读
神经网络
人工神经网络(1943)
使用数学模型对神经元进行建模
感知机 (1957)
最初的人工神经网络模型不能处理线性不可分问题
反向传播(1974)
采用反向传播法训练一般的人工神经网络
深度学习
多层感知器 (1980)
采用多个隐含层代替代感知器的单层结构最早的深度学习网络模型
RNN (1982)
处理序列输入和输出场景
LSTM (1997)
通过门控单元解决长序列数据训练的困难性
Word2vec (2013)
用于文本产生词向量
GRU (2014)
简化LSTM结构并提升表现
seq2seq (2014)
采用编码器-解码器架构用于机器翻译和对话系统
Transformer(2017)
自注意力机制,通过编码器-解码器结构处理自然语言
大语言模型
GPT-1 (2018)
预训练语言模型基于Transformer和自回归机制生成文本
BERT (2018)
预训练模型,通过masking预测句子中缺失的词汇
GPT-2 (2019)
更大的预训练模型,能够输出高质量文本
GPT-3 (2020)
目前最强的通用NLP语言模型,可以完成多种领域的自然语言任务
Instruct GPT(2022)
ChatGPT背后的语言模型通过人类指令(prompt)来调整AI生成的内容
GPT-4 (2023)
大型多模态语言模型接受图像和文本输入专业和学术基准上表现出人类水平的表现
NLP语言模型的发展历程
ChatGPT的火爆出圈,让大家对自然语言处理(Natural Language Processing)语言模型的发展历程产生了浓厚的兴趣。本文将从深度学习在NLP领域的发展历程,到大语言模型的发展历程,再到大语言模型的未来展望,带你一起了解NLP语言模型的发展历史。
自然语言
想必很多人对ChatGPT涌现出的多领域能力感到震撼,尤其是它能很好使用人类的语言与人类沟通,光这一点就已经足以让很多人感到恐怖了。机器会使用自然语言与我们沟通,为什么会让我们感觉到恐怖?
想象一下,某天我们的宠物突然会说话,会不会让我们感到难以接受?这是因为从数万年前我们的祖先智人走出非洲并统领地球开始,我们就一直是地球上唯一会使用语言的物种,这是我们与其他物种的巨大差异,也是我们独特的优势所在。但现在这种优势被我们创造出的智能掌握了,这是我们感到恐怖的原因。
人类文明的高科技
人类从掌握语言,并以语言创造出文字,靠着这种超越其他物种的能力,人类从个体的单打独斗产生了复杂的协作关系,从而进入文明社会,这意味着我们靠语言与文字创造了可延续的文明。
现代经济、国家、法律、文化等都是人类集体共同想象的产物,这是智人与其他物种一个巨大的差异,而语言又是想象的载体,正是有了语言,我们才能够将想象力传递给其他人,从而创造出璀璨的人类文明。
从这个角度看,语言是人类最先掌握的高科技,也是人类文明存在的基础。
人工智能的一颗明珠
在人工智能发展之前,人类是地球上甚至宇宙中唯一拥有智慧的生物,但我们的智慧只能通过繁衍后代的方式传递给下一代,老实说这种方式太过低效。在人类发明计算机后,人类智慧从碳基生物传递给硅基生物成为了可能。
于是人工智能诞生了,人工智能是一门研究如何使机器能够像人一样智能的学科,这也是人类探索自身智慧的一门学科。
如何让机器拥有类人一样的智慧?第一步就是让机器能够理解人类的语言,这样机器才能够通过语言学习人类文明的知识,能与人类进行沟通,这也是人工智能领域的一个重要分支,这个分支被称为自然语言处理(Natural Language Processing,简称NLP),它是人工智能领域的一颗明珠。
如果机器能理解人类的语言,配合计算机视觉(Computer Vision),再给这个系统一套机器人的机械身体,那人类相当于可以像神一样创造新物种了。照这个方向继续发展,机械飞升也不是不可能。
而现在,我们创造出的智能也掌握了人类自然语言,这也许是我们进入智能文明的标志。
从神经网络到深度学习
要让机器能够理解人类语言,目前常用的方法是从机器学习开始。传统的机器学习方法有先天假定,需要人工确定数据特征(即特征工程),这可能需要大量的人工工作,并且存在错误假设的可能性。
在机器学习领域中,深度学习是一种基于神经网络的机器学习技术。其中的主要特点是能够自动学习数据的特征,通过将特征学习任务交给模型进行训练来实现自动学习过程。
机器学习一开始有很多路线,不过逐渐人们发现深度学习的效果非常好,很多机器学习领域要解决的问题都可以用深度学习解决。
神经网络
人工神经网络(1943)
使用数学模型对神经元进行建模
感知机 (1957)
最初的人工神经网络模型不能处理线性不可分问题
反向传播(1974)
采用反向传播法训练一般的人工神经网络
神经网络
神经网络是一种模拟人脑神经元网络的机器学习模型,它最早于1943年被提出,它使用数学模型对神经元进行建模,神经元是人脑的基本单元,它接收输入信号,经过处理后产生输出信号,神经元之间通过突触连接,突触可以增强或抑制神经元之间的连接强度。
最简单的感知器模型如下所示:
w1
w2
x1
y
x2
如果用数学公式来表达如下所示:
{
0
w
1
x
1
+
w
2
x
2
≤
θ
1
w
1
x
1
+
w
2
x
2
w
1
x
1
+w
2
x
2
≤θ
w
1
x
1
+w
2
x
2
θ
看起来不可思议,人类的智慧竟然可以用这么简单的公式来表达。这个公式的意思是,如果输入的两个信号的加权和大于阈值,那么输出1,否则输出0。这个公式就是感知器的数学模型,它是最早的神经网络模型。
单个的神经元很简单,但通过多个神经元的组合,就可以构成复杂的神经网络,这样的神经网络就可以解决复杂的问题。如下图所示是一个简单的神经网络:
w11
w12
w21
w22
w31
w32
x1
y1
x2
y2
y3
如何理解这个神经网络呢?首先,这个神经网络有3层,第一层是输入层,第二层是隐含层,第三层是输出层。输入层有两个神经元,隐含层有两个神经元,输出层有一个神经元。每个神经元都有一个阈值,每个连接线都有一个权重,这些权重和阈值都是需要训练的参数。
这个简单的神经网络通过叠加多个隐含层,就可以完成复杂的人工智能任务,比如识别图片中的手写数字,这个流程如下:
第一层输入层接收图片的像素点作为输入信号,比如图片的大小是28x28,那么输入层就有784个神经元;
隐含层将输入信号进行处理,层层传递后给输出层产生信号;
输出层的信号就是图片的内容,比如输出层有10个神经元,那么输出层的信号就是10个数字,每个数字代表图片识别的概率,概率最大就代表整个神经网络的识别结果。
这个流程看起来很简单,但是神经网络的训练过程非常复杂,需要大量的数据和计算资源,所以在神经网络被提出后,很长一段时间没有得到应用。而且为了取得好的效果,神经网络的层数就需要很多,深度学习就是指神经网络的层数很多,所以深度学习的训练过程更加复杂。
深度学习
多层感知器 (1980)
采用多个隐含层代替代感知器的单层结构最早的深度学习网络模型
RNN (1982)
处理序列输入和输出场景
LSTM (1997)
通过门控单元解决长序列数据训练的困难性
Word2vec (2013)
用于文本产生词向量
GRU (2014)
简化LSTM结构并提升表现
seq2seq (2014)
采用编码器-解码器架构用于机器翻译和对话系统
Transformer(2017)
自注意力机制,通过编码器-解码器结构处理自然语言
深度学习
深度学习就是加深神经网络的层数,当然在特定领域的处理中,除了加深层数还需要处理特定的问题,比如在自然语言处理领域,就需要处理文本的序列问题,这个时候就需要采用RNN、LSTM、GRU、seq2seq、Transformer等模型。
这些模型并不是独立的,而是相互关联的,比如seq2seq模型就是由RNN和LSTM组成的,Transformer模型就是由seq2seq模型演变而来的。
和大家也许想的不同,当ChatGPT与人交流时,它会根据上下文不断预测并生成下一个单词。这个上下文包括了之前生成的所有单词,而不是一次性生成整个句子。我们可以通过如下数学公式来表示这个过程:
1
n
P
(
w
i
∣
w
1
,
w
2
,
…
,
w
i
−
1
)
P(w
1
,w
2
,…,w
n
)=
i=1
∏
n
P(w
i
∣w
1
,w
2
,…,w
i−1
)
该公式表示生成整个句子
w
1
,
w
2
,
…
,
w
n
w
1
,w
2
,…,w
n
的概率等于每个单词的生成概率之积(联合概率),其中每个单词的概率都是基于其前面所有单词来计算的。以上公式能给出单词序列发生的概率,这意味着语言模型可以通过概率来评估一个单词序列发生的可能性,也意味着可通过此概率评估一个句子在多大程度上是自然的单词序列。通过这种概率的计算,让ChatGPT这类语言模型能够产生连贯而自然的对话回复。
从文本到词向量
从上面神经网络的例子中,我们可以看到神经网络的输入和输出都是数字,而且神经网络的每个神经元都是一个数字,这些数字都是浮点数,所以神经网络的输入和输出都是浮点数。
但自然语言处理的输入输出都是文本,所以需要将文本转换为数字,这个过程就是文本的向量化,也就是将文本转换为向量。文本向量化的方法有很多,比如one-hot、TF-IDF、word2vec等。
word2vec是一种用于生成词向量的神经网络模型,它最早于2013年被提出。该模型主要用于自然语言处理任务中,如文本分类、文本生成等。相比于one-hot和TF-IDF,word2vec可以将文本中的每个单词转换为一个固定长度的向量,这个向量可以用于神经网络的输入。
有趣的是,词向量表示只是word2vec模型的副产品,word2vec模型的主要目的是通过训练神经网络来学习词向量,而不是生成词向量。word2vec模型通过训练神经网络来预测输入文本序列中下一个单词,当模型训练的能成功预测下一个单词时,说明神经网络已经学习到了文本中单词的语义信息,这个时候神经网络的隐含层(输入权重)就是输入文本序列的词向量,这也意味着在不同的语料库中,相同单词的词向量是不同的,但是相似的单词的词向量是相似的。
循环处理文本序列
将文本转换为词向量后,另一个问题是目前神经网络都是前馈神经网络,它们都是将输入数据从输入层传递到输出层,而没有反馈,所以无法处理序列数据,比如文本序列。于是就有了循环神经网络(RNN)。
RNN模型指的是循环神经网络(Recurrent Neural Network),它最早于1982年被提出。该模型主要用于自然语言处理任务中,如文本分类、文本生成等。循环神经网络通过使用循环结构,将当前时刻的输出作为下一时刻的输入,从而实现对序列数据的处理。
循环神经网络的每个神经元都有两个输入,一个是当前时刻的输入,一个是上一时刻的输出,这样就可以将序列数据的信息传递到下一时刻。循环神经网络的结构如下图所示:
…
X0
RNN
RNN
H0
X1
RNN
H1
X2
RNN
H2
Xt
Ht
Ht
如上图所示,循环神经网络可以从输入序列
X
X中逐个读取输入
X
t
X
t
,并将当前时刻的输入
X
t
X
t
和上一时刻的输出
H
t
−
1
H
t−1
作为输入,然后输出当前时刻的输出
H
t
H
t
,这样就可以将序列数据的信息传递到下一时刻。
不过原始的RNN模型存在记忆消失的问题,在长序列的情况下,模型无法学习到序列中较早的信息(可以简单理解为RNN在输入序列的循环过程中,在每次的输入转换中有信息的损失),所以后来又有了LSTM和GRU模型,这些模型通过使用门控结构(输入门、遗忘门与输出门),解决了RNN的记忆问题。
神经网络模型经常使用反向传播和梯度下降算法进行训练,以便更新模型参数来获得更接近真实值的输出结果。然而,由于存在梯度消失和梯度爆炸的问题,导致模型无法正确训练和收敛。在RNN模型中,这些问题尤为突出。
梯度消失是指在反向传播过程中,梯度值逐渐减小,最终使得模型无法训练。与之相对,梯度爆炸是指反向传播中梯度值变得极大,同样也会导致模型无法训练。了解这些问题后,我们需要寻找一些方法来解决它们。例如,可以通过采用梯度裁剪技术来解决梯度爆炸的问题;使用LSTM等网络结构可以有效缓解梯度消失的问题。此外,ReLU激活函数也被广泛使用,因为它能够防止梯度消失并加快训练速度。
从序列到序列
有了RNN模型,就可以处理序列数据了,但是RNN模型只能处理单个序列,而不能处理两个序列之间的关系,于是就有了seq2seq模型。
seq2seq模型指的是序列到序列模型(Sequence-to-Sequence Model),它最早于2014年被提出。该模型主要用于自然语言处理任务中,如机器翻译、对话系统等。
在传统的机器翻译方法中,通常采用基于短语或句法分析的方法将源语言句子转化为目标语言句子。这种方法存在大量的人工特征工程和规则定义,效果依赖于语言专家和领域知识,难以适应不同的语言和领域。
seq2seq模型通过使用编码器-解码器架构,将两个序列之间的关系转化为两个向量之间的关系,从而实现对两个序列之间的关系进行建模。在不需要手工特征工程的情况下,自动地学习源语言到目标语言之间的映射关系。其中,编码器将源语言序列映射到一个固定长度的向量表示,解码器利用这个向量表示生成目标语言序列。这种端到端的训练方式具有很强的可扩展性和泛化能力,并且随着数据量的增加,模型表现也会逐渐提升。
因此,seq2seq模型既解决了传统机器翻译方法中需要注重规则和特征工程的缺陷,同时也支持了更加复杂和灵活的自然语言处理任务。
它的架构如下所示:
如上图所示,这是一个翻译汉语到英文的语言任务,输入的是汉语句子你好,输出的是英文句子Hello。首先,将输入的汉语句子经过编码器,得到一个固定长度的向量表示,然后将这个向量表示输入给解码器,解码器将这个向量表示转化为英文句子。
以下是Seq2Seq模型在生成文本时的训练与生成流程:
(
x
1
,
x
2
,
…
,
x
T
)
X=(x
1
,x
2
,…,x
T
)。
编码器
将输入序列
X
X输入给编码器(Encoder),编码器使用RNN(通常采用LSTM或GRU)将词向量序列转化为单个上下文向量
z
z,即最后一个时间步的隐藏状态。这个向量包含所有输入信息的综合表示,它将被用于之后的解码器中。
(
y
1
,
y
2
,
…
,
y
T
′
)
Y=(y
1
,y
2
,…,y
T
′
)。这里的输出序列长度不一定和输入序列相同。例如,在翻译中,从英语到意大利语的句子长度可能会改变。
训练
对于每个目标输出序列
Y
′
Y
′
(即参考答案),训练的目的是尽可能地使生成的输出序列
Y
Y最大程度地接近
Y
′
Y
′
。因此,我们需要定义一个损失函数,用来衡量以
Y
Y作为输出时产生的误差,通常使用交叉熵损失函数。
生成
在生成时,我们使用训练好的编码器和解码器来处理新的输入,并生成一个输出序列。具体地,首先将输入字符串经过相同的预处理转化为词向量序列
X
X。然后,将
X
X拿去给编码器,得到上下文向量
z
z。最后,利用解码器对
z
z进行解码,即可得到生成的输出序列
Y
Y。
seq2seq模型的优点是可以处理变长的序列,但是它也存在一些缺点:
编码器只能通过一个固定长度的向量来表示输入序列,这个向量包含了输入序列的所有信息。因此,编码器的向量表示可能会存在信息损失,这会影响到解码器的性能。
编码器和解码器都是使用RNN来实现的,因此它们都存在梯度消失和梯度爆炸的问题。这会导致模型的训练变得困难,同时也会影响到模型的性能。
编码器和解码器都是使用RNN来实现的,因此它们都是串行的结构,无法并行计算,导致训练速度较慢。
seq2seq模型的缺点主要是由于RNN的缺点导致的,因此我们需要一种新的模型来解决这些问题,这就是Transformer模型。
注意力是全部
RNN网络模型的问题主要在于,串行处理输入序列,无法并行计算,同时也无法捕捉到输入序列中的长距离依赖关系。而Transformer模型则是完全基于注意力机制来实现的,因此它不仅能够并行计算,而且能够更好地捕捉输入序列的长距离依赖关系,从而提升模型的性能。
在RNN中,序列中的每个单元都是按顺序依次进行计算的,后一个单元的计算需要等待前一个单元计算完成后才能开始,因此无法并行化计算。而在Transformer中,自注意力机制的计算可以并行计算,因为每个单词的表示向量是独立计算的,它们的计算不需要等待其它单词的计算完成,因此可以同时进行。此外,注意力权重的计算也可以并行化,因为每个单词与其它单词的相似度可以同时计算,因此Transformer的自注意力机制可以更快地处理大规模序列数据。
对比RNN,举例来解释Transformer的注意力机制:
假设我们有一组输入序列,包含5个单词:“I”, “like”, “to”, “eat”, “pizza”。
我们将每个单词转化为向量表示。可以使用词向量来表示每个单词。
将每个单词向量应用于三个全连接层,以获得表示向量。每个全连接层都采用不同的权重矩阵,用于捕捉不同方面的单词特征。
为了计算向量之间的相似性,我们将每个表示向量与相邻单词的表示向量计算点积。对于句子中的每个单词,它将与前后单词的表示向量进行点积。
我们将每组点积值传递到Softmax函数中,以得出每个单词与相邻单词的注意力权重。这个步骤计算了每个单词与相邻单词的重要性,并为序列中的每个单词分配了注意力权重。
将每个单词向量与其对应的注意力权重相乘,然后将它们相加。这将形成一个自注意力池化向量,该向量是所有输入单词的加权和。这个自注意力池化向量可以更好地捕捉单词之间的语义关系,同时维护整个输入序列的完整性。
通过这种方式,自注意力机制允许模型更加全面地考虑输入序列中不同单词之间的关系,并获取单词之间的上下文信息,从而为序列到序列转换任务提供更好的表现力和准确性。
Transformer模型的架构如下所示:
Source: Attention Is All You Need
Source: Attention Is All You Need
如上图所示,Transformer模型的编码器和解码器都使用了自注意力机制,它们的输入都是整个序列,而不会像RNN那样串行处理序列。因此,Transformer模型可以并行计算,从而提升模型的训练速度。
Transformer模型将语言模型的训练带入了大语言模型时代,因为它的训练计算复杂度不会随着输入序列的长度而指数增加,因此可以训练更大的模型,从而提升模型的性能。
同时随着GPU计算能力的提升,Transformer模型的训练时间也大大缩短,因此Transformer模型的训练成本也大大降低,于是一个大语言模型的时代拉开了序幕。
Source: Harnessing the Power of LLMs in Practice: A Survey on ChatGPT and Beyond
Source: Harnessing the Power of LLMs in Practice: A Survey on ChatGPT and Beyond
大语言模型时代
GPT-1 (2018)
预训练语言模型基于Transformer和自回归机制生成文本
BERT (2018)
预训练模型,通过masking预测句子中缺失的词汇
GPT-2 (2019)
更大的预训练模型,能够输出高质量文本
GPT-3 (2020)
目前最强的通用NLP语言模型,可以完成多种领域的自然语言任务
Instruct GPT(2022)
ChatGPT背后的语言模型通过人类指令(prompt)来调整AI生成的内容
GPT-4 (2023)
大型多模态语言模型接受图像和文本输入专业和学术基准上表现出人类水平的表现
大语言模型
在Transformer模型出现后,这种架构启发了OpenAI的研究人员,于是他们在2018年提出了GPT-1模型,这是一个基于Transformer的预训练语言模型,它使用了自回归机制来生成文本。
生成式预训练
Generative Pre-Training (GPT)语言模型的训练过程是半监督的,使用了无监督预训练和有监督微调的组合方法,旨在学习一个通用的表征,以便在各种任务中进行迁移学习。
该模型使用Transformer架构右侧的解码器(Decoder)部分,在无标注数据上使用语言建模目标进行初始参数学习,然后使用相应的监督目标来适应目标任务(用有标注的数据训练并微调模型)。在转移过程中,使用从遍历式方法推导出的特定于任务的输入适应进行微调,以实现最小的对预训练模型架构的更改。该模型在自然语言推理、问答、语义相似性和文本分类等四个类型的语言理解任务上都获得了优异的表现。
GPT的创新之处在于:
通过Transformer架构,它实现了并行计算,提高了模型训练速度。同时,该架构还具备自回归机制,支持文本生成任务;
结合预训练和微调技术,GPT可以应用于多种NLP任务,并支持零样本学习;
GPT不仅支持多模态、多语言、多任务和多领域任务,而且在这些领域中表现优异。
GPT使用了Transformer模型的右侧解码器,因为其被证明是一种非常适合自然语言处理任务的模型,并且能够在处理长序列时表现出色。
Transformer模型中有两个主要的组件:编码器和解码器。而编码器被设计用于从输入序列中提取信息,而解码器则可从该信息生成输出序列。 在机器翻译等任务中,需要将源语言序列(即输入)转换为目标语言序列(即输出)。因此,传统的神经机器翻译系统使用编码器-解码器架构来进行这些任务。
然而,在语言生成任务如文本生成、对话模型等方面,只需要根据历史上下文生成新的单词/字符串。 在这些任务中,只需要生成一个序列,而不是翻译两个不同语言之间的不同的序列信息。因此,使用Transformer模型中解码器部分的权重,可以更好地拟合文本生成类任务,同时也可以更好地利用它的多头自注意力机制和残差连接来处理长序列,并且可以解决学习时间步骤之间长距离依赖关系的挑战。
大力出奇迹
人类的大脑神经元数量大约是1000亿个,这些神经元能产生数万亿个连接,而GPT-1模型的参数数量只有1.17亿个,这意味着GPT-1模型的参数数量只有人类大脑神经元数量的0.1%。
不知道OpenAI是否受到了这个启发,于是他们在2019年提出了GPT-2模型,这是一个更大的预训练模型,它的参数数量是GPT-1模型的10倍,达到了15亿个,在2020年发布的GPT-3模型的参数数量是GPT-2模型的100倍,达到了1750亿个,从数量上来看,GPT-3模型的参数数量已经超过了人类大脑神经元的数量。
于是,奇迹出现了,GPT-3模型在专业和学术基准上表现出了人类水平的表现。这个奇迹的出现是因为GPT-3模型使用了深度学习和大规模预训练技术,它被训练了海量的自然语言数据,并利用自监督学习方法进行强化,使其能够产生惊人的语言表达能力。此外,GPT-3还具有令人难以置信的创造力,它能够生成准确、流畅的文章、诗歌和音乐,甚至在游戏、绘画等其他领域也能发挥出惊人的创作能力。
虽然GPT-3模型已经展示了令人瞩目的性能,但它仍然存在一些局限性,比如对于某些特定领域知识的理解还不够深入,同时语言模型的运行需要庞大的计算资源和存储空间,这也增加了该技术应用的成本。未来,我们可以期待更加智能、灵活、高效的语言模型的出现,它们将会给我们带来更多新的可能性和优秀的应用场景。
然而,GPT-3的强大并非仅凭借大力出奇迹而来,因为许多其他参数数量超过人类大脑神经元数量的语言模型却没有表现出比GPT更好的效果。可见,GPT-3之所以拥有如此巨大的影响力,是由于其强大的表现和开创性的技术贡献所决定的。
未来展望
大语言模型的出现,为我们提供了一种新的思路,即通过大规模预训练和微调技术,让模型具备更强大的语言理解能力,从而能够应用于更多的NLP任务。未来,我们可以期待更加智能、灵活、高效的语言模型的出现,它们将会给我们带来更多新的可能性和优秀的应用场景。以下是笔者对未来语言模型发展的一些展望:
智能编程:它是将大语言模型作为推理引擎,无需编程即可完成各种用户提交的任务。例如,当用户请求播报最近热点新闻简报时,大语言模型可以自动规划任务并使用必要的工具来完成任务:获取新闻网站的热点新闻RSS地址,然后爬取新闻内容,总结新闻,并使用文字转语音接口合成语音。与传统的编程方式相比,这种方式更加灵活和高效。我们只需要设计一套内置的规则,大语言模型就能够动态规划并完成任务需要的步骤和子任务的工具,从而大幅降低人工编程的成本。
多模态能力:它是尝试让大语言模型能理解语音、图片、视频等多模态内容。Transformer模型在这个方面已经发挥了至关重要的作用,并使得GPT-4具有了多模态能力。未来的研究将集中于设计更加灵活、适应性更强的模型,使其能够处理不同类型和媒体形式的内容。
设备端语言模型:目前大语言模型只能在云服务中运行,未来的一个重要方向是将其部署到移动设备上,使其能够在本地运行。这样做的好处是可以保护用户隐私,同时也可以降低网络延迟,提高用户体验。这些模型相对于云服务中的模型来说更加垂直,能在设备端运行,就像一个智能的助手一样,能够帮助用户完成各种任务。
隐私安全问题:它是企业在应用大语言模型过程中面临的重要问题,一些企业难以接受将自己的重要数据上传给大型语言模型。因此,一方面可以考虑使用脱敏的方式为模型提供提示语,另一方面,也可能会出现聚焦于本地运行的开源或商业化大语言模型,从而保护企业的隐私。
版权问题:目前的大语言模型都是基于各类公开数据训练的,而这些数据的版权关系可能比较复杂:比如,如果一个企业使用了某个大语言模型,而这个大语言模型生成了一些侵犯他人版权的内容,那这时候该怎么处理?这些问题都需要在未来的研究中考虑。比如GitHub Copilot就是一个商业化的大语言模型,它的训练数据来自于GitHub上的开源代码,而这些开源代码在训练过程中并未做代码许可证的过滤,这可能就引发了一些版权的问题。
泛化及可解释性问题:随着技术的不断进步,我们期望机器能够像人类一样具备广泛的推理和应用能力,同时也能够给出解释和原因,以便更好地理解机器的决策和思维过程。
安全问题:随着人工智能在社会生活中的广泛应用,越来越多的机器学习算法和模型将牵涉到重要的决策过程,例如医疗、金融等领域。因此,对机器学习的安全性进行更加深入的探究,并开发相应的安全技术,将是未来研究的一个重要方向,以确保机器学习的应用是可靠和安全的。
高效节能:它是让机器学习模型在仅有少量数据输入和能耗消耗的情况下像人脑一样学习新知识。虽然深度学习在自然语言处理领域取得了重要进展,但我们仍需寻找更高效、节能的方法来应对大规模的学习任务。为了降低成本,未来的人工智能发展可能需要从更深层次上改变算法和计算架构,在保证高效性的同时实现更低的能耗。
人工智能的崛起,给予了我们像上帝一样塑造新生命的力量。我们期待未来的AI不仅仅是人类的伙伴,更是我们和宇宙之间的桥梁。因为最终我们将理解:科技没有善恶之分,只有我们对待它的方式才会体现出道德的坐标。正如电影《星际穿越》中所说:
Love is the one thing we’re capable of perceiving that transcends time and space.
进阶阅读
视频
Transformer论文逐段精读
GPT,GPT-2,GPT-3论文精读
BERT论文逐段精读
InstructGPT论文精读
Open Pretrained Transformers - Susan Zhang | Stanford MLSys
ChatGPT的前世今生
论文
Attention Is All You Need
Improving Language Understanding by Generative Pre-Training
BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
Language Models are Unsupervised Multitask Learners
Language Models are Few-Shot Learners
Training language models to follow instructions with human feedback
图书
深度学习入门
深度学习进阶
动手学深度学习
复杂
更新时间: 25个月前 版本: bd8f3e7e5
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
AI驱动开发:从Prompt到Product(直播)
我与ChatGPT结对编程的体验
ChatGPT应用开发小记
我的AI阅读助手
AI降临
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
ChatGPT应用开发小记
7 April 2023 28:3 | 6173 字 | 17589 次阅读
什么是X
chatGPT
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
AI背景知识
生成式预训练模型(GPT: Generative Pre-trained Transformer)
大语言模型(LLM: Large Language Model)
词向量检索
Prompt
Embedding
基于chatGPT的应用类型
套壳chatGPT应用
增强类chatGPT应用
Prompt类应用
myGPTReader应用架构
myGPTReader实现过程
最后的思考
如何开发一个基于chatGPT的应用?答案就在问题里,那就是用chatGPT来开发基于chatGPT的应用。本文以笔者的一个开源项目 myGPTReader 为例,分享我是如何基于chatGPT去开发这个系统的,这个系统的功能见这篇文章:我的AI阅读助手。
为了探索chatGPT辅助开发的可能性,我在项目开发之初就记录了与chatGPT对话的过程,将近一个月的开发日志都记录在 ChatGPT Driven Development Record 这个文档里。
从最终的结果看,我的角色从开发变成了产品设计与测试,整个项目的代码我个人亲自编写的可能不到10%,从后端服务到前端页面,甚至连Logo都是AI来完成的,Logo的设计过程我也记录到了 Midjourney Driven Design Record 这个文件了。
更有意思的是,这个项目因缺少部署文档,使一些不了解技术但又想独立部署该项目的网友犯难,但最终他们与chatGPT反向去研究这个项目,竟然也能成功部署。
总之这是一个chatGPT含量极高的项目,无论是开发过程还是最终的功能。
AI背景知识
chatGPT是一个基于GPT-3.5/4的开放API,它的背后技术是基于Transformer网络的大型无监督语言模型,通过预训练和微调两个阶段的训练,能够学习自然语言的规则和结构,并且在各种下游任务上表现出色。从更大范围来说,chatGPT是属于自然语言处理(Natural Language Processing)领域的一个大语言模型(Large Language Model)。
生成式预训练模型(GPT: Generative Pre-trained Transformer)
我:你能给我说说chatGPT背后的技术吗?比如大语言模型,还有训练过程。
chatGPT:当谈到ChatGPT时,背后的技术通常是指OpenAI GPT(Generative Pre-trained Transformer)模型,这是一种使用Transformer网络架构进行预训练的大型语言模型。GPT是基于无监督学习技术构建的,该技术可以在没有标签的大量数据集上进行训练,以自动发现数据中的模式和结构。
GPT模型的训练过程分为两个阶段:预训练和微调。
预训练阶段使用一个大规模的无标签语料库,例如维基百科、新闻文章、小说等,通过预测给定文本序列中下一个单词的任务,对模型进行训练。这个任务被称为“语言模型”,因为模型被训练为理解自然语言中的上下文和语法规则,以便能够预测下一个单词。在这个过程中,GPT模型可以学习到自然语言的规则和结构,形成了通用的语言表示。
在预训练完成后,微调阶段使用少量的标记数据对模型进行微调,以适应特定的下游任务,例如问答、文本分类、生成对话等。微调过程通过反向传播梯度更新模型的权重,以最小化特定任务的损失函数。微调的过程可以增强模型对特定任务的理解能力,并且可以使模型在下游任务上表现更好。
总的来说,GPT模型的背后技术是基于Transformer网络的大型无监督语言模型,通过预训练和微调两个阶段的训练,能够学习自然语言的规则和结构,并且在各种下游任务上表现出色。
大语言模型(LLM: Large Language Model)
我:大语言模型、GPT、Transformer三者的关系是?
chatGPT:大语言模型、GPT、Transformer三者是密切相关的概念,它们在自然语言处理和深度学习领域有着广泛的应用。具体关系如下:
Transformer是一种基于注意力机制的神经网络架构,最初是为了机器翻译任务而设计的。它由编码器和解码器组成,可以对序列数据进行建模,适用于在输入和输出之间存在长距离依赖关系的任务。
大语言模型是建立在Transformer架构基础上的语言模型,常见的大型预训练模型包括GPT、BERT等。大语言模型通过在大规模文本语料库上进行自监督预训练,能够捕捉到语言中的复杂结构与信息,从而在下游任务中取得很好的效果。
GPT (Generative Pre-trained Transformer)是一个基于Transformer架构和自监督预训练的句子级别自动生成模型,通常用于生成式任务,比如文本自动生成、摘要生成、对话系统等。
因此,可以说,大语言模型是应用了Transformer架构并使用自监督预训练技术的模型,而GPT则是基于大语言模型构建的具体任务模型,两者都是Transformer架构在语言建模方面的应用。
一个奇怪的问题是,基于Transformer这种设计之初是为了机器翻译任务的模型,为什么在编写代码方面也表现良好?
我:为什么基于Transformer的GPT模型不仅可以用在翻译上,也能在在编写代码的任务上表现良好?
chatGPT: 1.基于文本流的代码生成能力:现代软件开发通常包括对源代码的有效管理。生成源代码的技术需要考虑各种软件开发标准、API接口以及所有相关部分间相互影响的复杂性。GPT模型强大的自然语言理解能力使其能够有效生成高质量的源代码。
2.上下文理解:代码通常是一个非常上下文相关的环境,特别是在功能相似但又具有微妙区别的不同代码块之间。 GPT模型能够根据上下文生成出更加匹配语法、且更加可读的代码。
3.词向量表达:源代码的语法是有结构的,并且它通常被认为属于DSL的元素构成的单词阵列。它的语法规则清晰,也容易编写非常明确的语义。GPT通过预训练的方式将从输入数据中获得的词向量检索作为核心基础,使用语言模型来编写高效、正确的代码。
因此,GPT模型在生成高质量源代码的任务方面也有很好的应用价值。
这里提到的一个关键技术:词向量检索,这在后面也会有所提及。
词向量检索
我:什么是词向量检索?
chatGPT:词向量检索,也称为向量空间模型检索,是一种文本检索方法,其基本思想是将输入的文本转换为向量形式,并使用数学模型对文本相似度进行计算,从而寻找匹配的文本集合。
通常情况下,词向量检索分为以下几个步骤:
文本预处理:对原始文本进行分词、去除停用词等操作,以得到可供后续处理的单词列表。
向量表示:利用某种算法(如word2vec)将每个单词映射为一个向量。这些向量通常具有较高的维数,并包含了单词的语义信息。
查询处理:将用户输入的查询文本转换为向量表示,与预处理过的文本向量进行相似度计算,以得到最相关的文本。
结果呈现:将计算出来的相似度得分排列,呈现给用户最相关的文本列表。
词向量检索在自然语言处理领域被广泛应用,常用于搜索引擎、推荐系统等场景。它能够通过对文本的向量化处理,更准确地衡量文本之间的相似度,达到更精确和快速的文本检索效果。
Prompt
我:什么是AI中的Prompt?
chatGPT:在AI中,Prompt指的是提供给模型的文本或指令,可以用来初始化和引导模型生成一系列类似自然语言的输出。Prompt通常由一个或多个片段组成,其中每个片段都是一种语言形式或主题,用于定义AI模型需要生成的文本的内容和样式。
Prompt可以以多种方式使用,例如:
作为AI机器人对用户的响应:当一个用户与AI交互时,机器人会根据用户输入的信息和之前的上下文来生成回答。Prompt可以用作机器人生成响应的指南,确保AI产生的回复与预期的主题和风格相一致。
作为生成AI初始文本的输入:某些AI工具(如GPT-3)可以根据给定的Prompt生成文本。这可以用于快速创建文章、电子邮件、Twitter帖子等等,并且可以扩展已有的草稿或以不同的风格和语言复述相同的消息。
总之,Prompt在AI中是非常重要和有用的,因为它们可以让模型更加定向地生成特定类型或主题的字符串或句子。
我:为什么一定要用Prompt?Prompt未来还会存在吗?
chatGPT:在AI中,“Prompt”是指提供给模型的输入文本,它是用来启发模型生成输出的。Prompt通常是一些简短的话语或问题,向模型传递一些先验知识或上下文信息,以帮助模型更好地理解和执行任务。
在许多自然语言处理(NLP)应用中,prompt对于准确预测输出至关重要。例如,一个对话系统可能需要给出一个明确的提示,让用户提供足够的上下文信息来回答问题,或者一个分类器需要在prompt下面添加一个标签,以便识别正确的类别。 虽然在AI中,Prompt广泛使用并且非常有用,但未来是否会一直使用它们取决于技术的演进。可能会出现新的方式帮助模型更好的理解上下文信息,消除Prompt所起到的作用,但是现阶段仍然有很多领域弥补不了这个作用,因此Prompt仍然是一种不可替代的输入形式。
Embedding
我: openAI的embedding是什么?
chatGPT: OpenAI的Embedding是一种将词语或句子转换成密集向量的技术。这些向量可以用于自然语言处理任务,如文本分类,情感分析和机器翻译等。
在OpenAI的Embedding中,每个单词或句子都会被映射到一个向量空间中的点。与传统的one-hot编码不同,Embedding可捕获词语之间的相似性和关系,并使得机器学习算法能够更好地理解自然语言表达的含义。
例如,在情感分析中,通过使用Embedding,我们可以将具有相似情感倾向的单词或短语映射到相似的向量空间中的位置。这样,我们就可以使用这些向量来训练机器学习模型,以帮助它们更准确地预测文本的情感。
基于chatGPT的应用类型
由于chatGPT的功能实在过于强大,导致应用端能做的功能并不多,目前已知的一些应用类型有以下几种。
套壳chatGPT应用
这类应用主要是对接chatGPT的API或调用web端的接口,自身并没有太多的功能,只是将chatGPT的功能进行了封装。而这类应用又细分为两种:
工具类应用。需要用户提供自己的OpenAI API Key来使用,如很多APP或浏览器插件。
代理服务类。不需要用户提供自己的OpenAI API Key,而是通过代理服务来使用chatGPT的API。
增强类chatGPT应用
由于chatGPT本身不是联网的,无法获取最新的数据,且输入的Token也是有最大限制的,因此这类应用主要是为chatGPT突破这些限制而设计的,比如通过爬虫获取最新的数据,然后通过chatGPT来生成新的文本或总结这些文字,或者通过Embedding的方式将大量的文本向量化,然后把文本节点与向量化的组合存储到特殊设计的索引中,然后把用户输入的Token向量化后,通过词向量检索寻找相关的上下文,然后把这些上下文与用户输入的Prompt一同提交给chatGPT来获取答案。
要应用自己去处理这么多繁琐的工作显然是没有必要的,因为已经有非常好的开源项目去实现这些功能了,代表就是 jerryjliu/llama_index,使用这个库可以很容易实现这些增强功能,比如给chatGPT提供外部数据源,从而可以让它帮助我们分析网页、文档、新闻等等任务。
一些相关的应用:
myGPTReader:myGPTReader是Slack上的一个机器人,可以阅读和总结任何网页、文档(包括电子书)甚至来自YouTube的视频,它还可以通过语音与用户交流。
chatpdf:通过给chatGPT提供PDF文档,然后可以针对此文档进行问答,或者总结文档内容。类似的开源实现有:guangzhengli/ChatFiles与arc53/DocsGPT。
Copilot Hub:和上面的项目类似,不过多了能公开分享根据自己录入数据打造的问答机器人,比如能以乔布斯口吻回答问题的机器人。
binary-husky/chatgpt_academic:科研工作专用ChatGPT拓展,特别优化学术Paper润色体验。
这些应用大多以Embedding为核心,当然也都存在共同的问题:因为Embedding是根据相似度去搜索相关文本的,如果问题非常宽泛,那么很难有效搜索到相关的问题,因此使用这些应用需要用户提供尽可能相关的问题,这样才能得到比较好的答案,也就是说需要用户提供好的Prompt。
当然openAI官方也提供了插件市场,这些功能也可以在插件中实现,但目前插件仅能在网页版使用,而且插件现在并没有完全开放,只有一些特定的用户才能使用。
Prompt类应用
Prompt是GPT技术的过渡产物,也许在未来我们都不需要Prompt了,但目前这也算chatGPT衍生出来的一类应用。比如收集好的Prompt然后分享给其他人使用,这类应用代表有:OpenPrompt及很多awesome chatgpt prompts类的开源Markdown项目。
myGPTReader应用架构
既然这是一篇chatGPT的文章,分析应用架构的工作自然也是用chatGPT来完成,我们直接把整个项目的代码导入给chatGPT,然后让它来分析应用架构。
导入过程很简单,直接使用这个GithubRepositoryReaderDemo.ipynb里的代码即可,导入我们要分析的项目代码,在这里就是madawei2699/myGPTReader这个Repo。
导入后,让我们开始施展魔法吧!
Prompt: Give me the handle_mentions (in app/server.py) function call flow, output Sequence diagrams using mermaid and markdown and comment the function name.
Client
Slack
App
Client
Slack
App
Send Event Subscription Request
Send Verification Request
Send Verification Response
Send Event
Call handle_mentions()
Process Event
Send Response
Prompt: Give me the bot_process (in app/server.py) function call flow, output Sequence diagrams using mermaid and markdown and comment the function name.
GPT
FetchWebPost
BotProcess
App
User
GPT
FetchWebPost
BotProcess
App
User
Request
bot_process()
get_urls()
urls
scrape_website()
content
gpt_response()
response
response
Response
Prompt: Give me the get_answer_from_llama_file (in app.gpt module) function call flow, output Sequence diagrams using mermaid and markdown.
Synthesizer
SimpleDirectoryReader
LLMPredictor
GPTSimpleVectorIndex
App
User
Synthesizer
SimpleDirectoryReader
LLMPredictor
GPTSimpleVectorIndex
App
User
Get Answer from llama file
Get index from file cache
Return index
Load data from file
Return documents
Create index from documents
Return index
Query index
Get answer
Return answer
Speak SSML
Return audio
Return answer
你没看错,这些时序图是chatGPT根据我们的Prompt而绘制的。由于这并不是一个复杂的系统,所以时序图足以给我们展示整个应用的架构及模块之间的关系。就算是一个大项目,我们依旧可以通过这种方式来分析应用架构。
除了让它绘制图形来分析,我们还可以针对单个模块或函数让它以自然语言来解释代码的含义,比如:
Prompt: Explain get_answer_from_llama_file (in app.gpt module) function step by step.
chatGPT: get_answer_from_llama_file is a function that takes in a list of messages and a file path as parameters and returns an answer, the total number of tokens used by the LLM model, and the total number of tokens used by the embedding model.
Step 1: The function starts by formatting the dialog messages into a single string.
Step 2: It then gets the language code from the last message in the list of messages.
Step 3: It then gets the index name from the file path.
Step 4: It then checks if the index is already cached in the file cache directory. If it is, it loads the index from the cache. If not, it creates a new index from the documents in the file.
Step 5: It then gets the prompt template for the language code.
Step 6: It then uses the index and the LLM predictor to query the dialog messages with the prompt template.
Step 7: It then uses the synthesizer to speak the SSML asynchronously and save the audio to a file.
Step 8: It then gets the total number of tokens used by the LLM model and the total number of tokens used by the
myGPTReader实现过程
实现的过程我从第一个函数的实现开始到现在为止,已经将所有的Prompt及chatGPT的答案以日志的形式记录到了 ChatGPT Driven Development Record 这个文档里,这里就不再赘述了。
我在实现的过程保持着傻瓜式的开发方式,也就是chatGPT给我什么代码我就测试什么代码,遇到错误直接发给它让它来修复,在日志中也可以看出这种开发方式的特点。
不仅后端代码是这样实现的,连前端页面也是这么开发出来的。对于前端网页的开发,一个难点就是需要写好的Prompt告诉chatGPT我想要的页面效果,我甚至把我要实现的一个页面的竞品网站的源码发给它,让它给我用我确定的技术栈实现。我的前端技术栈是React + TailwindCSS,最开始我让它把工程创建的步骤告诉我,我根据它的流程去把工程框架搭建出来。之后就是告诉它我要创建的页面类型,页面有几个部分,每个部分的内容是什么,它就会自动帮我创建好页面,然后我再根据自己的需求去修改页面的内容。
前端的Prompt交互过程见 ChatGPT Driven Development Record # 2023-03-28。
最终的效果是这样的:myreader.io。
最后的思考
经过一个月与chatGPT结对编程,还实现了一个完整的应用,我对chatGPT的感受是:传统编程这个行业最终会消失,甚至比我们想象中的要更快。或许未来还会有一小部分人作为非物质文化遗产的传承者,古法手工编写代码。
当然不只编程,很多行业都会受到chatGPT这类技术的冲击,目前还很难评估它的边界在哪里,但是我相信它的边界会越来越大,最终会覆盖到我们现在所能想象的范围之外。
如果真是这样的未来,我们现在能够做什么才能保证自己的经验不会快速贬值甚至无效?这是一个值得思考的问题。
更新时间: 26个月前 版本: c80acb90a
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
AI驱动开发:从Prompt到Product(直播)
我与ChatGPT结对编程的体验
ChatGPT背后的语言模型简史
我的AI阅读助手
AI降临
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
SQLite的文艺复兴
19 January 2023 30:9 | 6635 字 | 16187 次阅读
分布式技术
Database • 分布式系统
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
本文已翻译成以下语言版本:
英文:SQLite Renaissance
SQLite的故事
SQLite的架构
SQLite的文艺复兴
Serverless / Edge Computing
Browser-compatible
Client/Server
OLAP
分布式
总结
SQLite的故事
第一次被SQLite震撼到的一个是关于它的测试代码,其源码有15多万行,但测试代码和脚本竟有九千多万行。作者Dwayne Richard Hipp是一个完美主义者,为了能自由的开发SQLite,他编写了底层的存储引擎、Parser、源码托管工具Fossil,除了C编译器和一些底层库如libc外,Richard几乎从零构建了SQLite所依赖的库或工具。
运行SQLite官网的Web服务器Althttpd也是Richard开发的,所有代码仅在一个C文件中,除了标准C库之外没有依赖任何其他代码库。
当然SQLite官网使用的数据库自然也是SQLite了,网页的动态数据甚至是由200多条SQL语句查询后在0.01秒内渲染完成的。
很难想象这种开发模式能够成功,但Richard做到了,他把SQLite推向了数十亿的设备与终端。由于绝大部分代码都是Richard一个人完成的,SQLite并不是一个开源协作的项目。虽然代码是开放的,但却并没有开源社区的贡献,甚至有人为此专门fork出了SQLite的开源协作版本:libSQL。
更多关于SQLite背后的故事可以听下这期Podcast:The Untold Story of SQLite。
SQLite的架构
SQLite是一个数据库软件,但与绝大部分数据库系统拥有完全不同的运行方式。大部分数据库(MySQL、SQL Server、PostgreSQL或Oracle)系统是Client/Server的架构,客户端通过特定的协议比如JDBC/ODBC与数据库服务器通信,数据库服务器通过监听某个Socket端口去接收客户端的查询请求,之后将结果返回给客户端。
与其他数据库网络通信的方式相比,SQLite是一个库,它是通过In-Process的方式来与应用程序通信的。当应用程序发出查询请求时,这些请求是通过函数调用的方式在与应用程序相同的线程内执行的。SQLite的数据库也是存放在磁盘上的单个文件。
与其他数据库相比,SQLite的优势就是快,尤其是执行一些小的SQL查询语句,这也是为什么SQLite的官网动态数据能通过200多条SQL语句来查询获取的。SQLite也没有N+1查询性能的问题,原因就是没有其他数据库网络通信的开销:
SQLite的架构如下:
SQLite Library
Compiler
Code Generator
Parser
Virtual Machine
/Bytecode Engine
Storage Engine
OS Interface
/VFS
B-tree
Pager
SQL
Operating System
Database File
Rollback Journal
SQLite的架构主要包括三大部分:编译器、虚拟机与存储引擎。当应用程序发起查询请求时,SQL语句首先会被编译器解析,然后生成字节码,最后通过虚拟机执行。虚拟机的执行过程中,会调用存储引擎的接口来读取或写入数据。
编译器的主要工作是将SQL语句解析成字节码,然后将字节码编译成可执行的机器码。编译器的主要组成部分是Parser与Code Generator,Parser负责将SQL语句解析成抽象语法树(AST),Code Generator负责将AST转换成字节码。查询计划的生成也是在Code Generator中完成的。
虚拟机是一个寄存器式虚拟机(Register-Based VM),它的主要工作是执行字节码。另外查询优化(Query Optimization)也是在虚拟机中完成的。对此部分感兴趣的读者可阅读这篇文章:How the SQLite Virtual Machine Works。
存储引擎的主要工作是读取或写入数据。存储引擎的主要组成部分是B-tree、Pager与OS Interface(也叫VFS):
B-tree:SQLite的索引以B-tree的数据结构存储,表数据是以B+tree的数据结构存储。对于此部分感兴趣的读者可阅读这篇文章:SQLite Internals: Pages & B-trees。
Pager(Page cache):B-tree模块与VFS模块之间的抽象层,提供读、写与缓存磁盘页面的功能。SQLite的原子性、隔离性、持久性都是通过Pager来实现的。
Pager提供了两种并发访问模式:回滚日志(Rollback Journal)与预写日志(Write-ahead Log)。与回滚日志相比,预写日志能提供更好的扩展性,能在写入数据时并发读取数据。在预写日志模式下,虽然一个数据库只能拥有一个预写日志文件(-wal文件),一次只能允许有一个写入线程去更新这个文件,但在配置busy_timeout后允许有多个写入线程同时运行,不过执行过程仍然是串行化的。
对于此部分我推荐阅读这两篇文章:How SQLite helps you do ACID与How SQLite Scales Read Concurrency。
OS Interface(VFS):为了提供跨操作系统的可移植性,SQLite使用了一个称为VFS的抽象层。VFS提供了打开、读取、写入和关闭磁盘文件的方法,以及其他特定于操作系统的功能。
以上就是SQLite架构的简单介绍,如果你还希望进一步了解内部实现细节,可以阅读这本开源电子书:SQLite Internals: How The World’s Most Used Database Works。
理解了这些,你就能够更好的了解下面这些把SQLite玩出花活的文艺复兴类开源项目了。
SQLite的文艺复兴
SQLite是个已经超过20岁高龄的并不新的软件,很多人对它的认知还停留在一个玩具类的数据库,拿它用来做一些简单的本地存储或测试,很少用来在生产系统中使用。但就是这样古老的软件,却长期在Hacker News中被热议,因为一些有趣的项目,让SQLite老树逢春、文艺复兴。
一些你可能觉得SQLite做不到的事:
SQLite单机百万TPS并发压测:Scaling SQLite to 4M QPS on a Single Server (EC2 vs Bare Metal)。
SQLite未来可支持多个并发写入:官方出品的SQLite HC-tree,尝试替换老的B-tree数据结构,能实现多写多读、分布式节点复制与更大的数据库大小限制,压测结果很不错。
Serverless / Edge Computing
将静态页面发布到CDN中,然后使用API提供动态更新的能力,这种Jamstack架构的Serverless应用能给业务系统带来极高的伸缩性。这种架构的限制在于,数据需要存放在单独的托管数据库中,并且费用不便宜,而且数据库可能会成为一个性能瓶颈,因为当业务系统部署到多区域时,单节点的数据库的网络开销是个不容忽视的问题。
那如果数据库与业务系统实例在同一个服务器上运行呢?🤯
Server - Frankfurt
DB
App
Server - US East
DB
App
Server - Singapore
DB
App
SQLite本身就是一个Serverless的数据库,它与应用程序运行在同一进程中。相比其他数据库,同样运行在与应用程序同一个服务器中,SQLite都比那些C/S架构网络通信类的数据库要更快。唯一的问题是,如何解决多实例读取与写入同一数据库?针对此问题有以下解决方案:
Litefs,此项目是Litestream作者加入Fly.io后开发的新项目。
Litestream能通过接管SQLite检查点过程(checkpointing process)从而控制对wal日志文件的管理,在预写日志(Write-ahead Log)模式下,litestream能持续的复制wal日志文件到备份的位置如S3中,从而实现了在线流式备份SQLite数据库文件。具体可以查看此文档介绍:How it works - Litestream。
Litefs相比Litestream要更进一步。Litefs通过提供一个基于FUSE的文件系统给SQLite做VFS层,当Leader节点要更新数据库时,该节点的SQLite的Pager模块要调用VFS层写入数据时,Litefs能够在页面级别复制与这个事务相关的页面集合,通过把这些页面集合打包成一个LTX数据格式的文件包,然后通过HTTP协议将这个文件包发送给只读节点,完成了数据的跨节点同步。
在Litefs的分布式集群中,只有主节点能够写入数据,只读节点可以通过将主节点地址转发给客户端,让客户端只在主节点写入数据。主节点的选举通过获取Consul的分布式租约(Distributed leases)来达成共识,也可以设置静态的主节点。
具体的架构参考这篇文章:How LiteFS Works · Fly Docs。一个部署案例可以参考这篇文章:I Migrated from a Postgres Cluster to Distributed SQLite with LiteFS。
另外Cloudflare也推出了类似的商业解决方案Cloudflare D1。
Browser-compatible
SQLite可以通过WebAssembly(WASM)技术运行在浏览器中,以下两个项目能让前端代码在浏览器中发起SQL查询:
sql.js:JS代码导入该库后,通过网络请求下载SQLite数据库文件到浏览器内存中,即可发起SQL查询获取数据结果。
absurd-sql:和sql.js的区别在于,该项目可以使用浏览器的IndexedDB作为持久化存储,能对SQLite数据库文件进行读写操作。
在浏览器中使用SQLite有什么好处?以我的这个Invest Alchemy开源项目为例,它是一个ETF投资组合管理系统,它需要管理多个ETF投资组合,每个ETF投资组合的所有数据都存放在一个SQLite数据库中,而这个数据库文件存放的位置是在AWS S3中。每天都会有一个定时程序自动下载AWS S3桶中所有的SQLite数据库,之后更新这些组合的数据,最后再上传至S3中。当用户浏览某个投资组合的页面,以这个投资组合为例,该页面在初始化渲染时会先从S3中下载SQLite数据库到浏览器内存,之后使用sql.js初始化SQLite数据库,最后发起多个SQL查询获取数据结果,然后渲染页面。
这种架构的好处在于,浏览器页面只需要发起一次查询请求,就可以获取整个组合的全部数据。如果使用传统的数据库,一方面是费用的问题,另外一方面每次查询都需要经过网络传输,这样会增加页面的加载时间。
最后,SQLite这种单数据库文件的方式,带来了很好的隔离性。比如在Invest Alchemy中,一个数据库代表一个投资组合,当然也可以把某个用户的所有个人数据存放至一个数据库中,再把这些数据库存放在AWS S3中不同的目录中,这样就可以很好的隔离不同用户的数据。
Client/Server
SQLite也可以作为一个Client/Server架构的数据库。当然这种用法已经失去了SQLite的优势,增加了网络开销,但在某些场景可能有用,比如作为一个只读数据源,或者作为一个数据缓存,这种费用开销应该比较低。
postlite:一个网络代理库,支持PostgreSQL的通信协议,后端使用SQLite作为存储。
sqlite3vfshttp:一个SQLite VFS,支持通过HTTP协议访问SQLite数据库文件。相比sql.js需要下载整个SQLite数据库文件,该库只需要通过客户端指定HTTP range头即可获取指定范围的数据,在一个大的数据库文件中,这种优化可以节省大量的网络开销。
Cloud Backed SQLite:官方支持的云端SQLite,支持Azure和GCP,可以通过Storage Client直接读取或写入数据库,无需下载整个数据库。
OLAP
SQLite典型的用法是作为一个OLTP数据库,很少用它做OLAP。因为它的表数据存储是行式而非列式。为此duckdb填充了这个空白,和SQLite相似的架构,只不过是列式存储,非常适合OLAP的业务场景。
不过SQLite在3.38.0版本中通过布隆过滤器(Bloom Filter)提升了大型分析查询语句的性能,这个特性也是为了支持OLAP的业务场景。当然更近一步的优化及与duckdb的对比可参考这篇论文:Introducing a Query Acceleration Path for Analytics in SQLite3。
分布式
SQLite这种In-Process的架构看起来与分布式数据库完全不相关,而且分布式数据库本身就是一个非常复杂的领域,分布式带给数据库弹性扩容及高可用的能力,由于分布式事务的复杂性,常规的做法是尽可能通过单机垂直扩容的方式来解决扩容的问题,实在没办法再通过横向扩容(分片)的方式来解决。但依旧有以下令人惊艳的项目把SQLite带向了分布式的领域。
rqlite:使用Raft解决分布式集群节点达成共识的问题,写入是通过Leader节点完成的,其他复制节点可以将写入请求透传至Leader节点,读取则是通过任意节点完成的,所以这也算是一种Leader-Replica的分布式架构风格。
rqlite并没有提高写入的吞吐量,因为写入还是Leader完成的并且有额外的一致性检查开销,相比In-Process的单机SQLite,写入的吞吐量会有所下降。
rqlite的数据API是通过HTTP公开提供的,所以它是一种Client/Server的架构风格。
rqlite的节点间同步是通过复制命令的方式实现的,比如当写入命令发至Leader节点时,一旦命令提交至Raft Log,Leader节点会将写入命令复制至其他节点。
设计文档见此:Rqlite Design。
mvsqlite:此项目的精妙之处在于其通过FoundationDB作为SQLite的VFS层,极大的利用了FoundationDB提供的分布式特性(乐观无锁并发、分布式事务、同步异步复制、备份与恢复),实现了一个支持MVCC的分布式SQLite,不仅是Leader-Replica,还实现了多节点写入的并行化,提高了写入的吞吐量。由于其实现了MVCC,所以甚至能实现在数据库层面的时间旅行(Time travel)。
推荐阅读作者的这两篇文章了解实现细节:1. Turning SQLite into a distributed database,2. Storage and transaction in mvSQLite。
与同样通过VFS模块实现分布式的Litefs相比,mvsqlite需要额外部署FoundationDB集群与mvstore无状态实例,所以部署与运维成本更高。
有趣的是,FoundationDB在7.0.0之前SSD存储引擎使用的是SQLite,不过在这之后,FoundationDB实现了自己的存储引擎Redwood。
cr-sqlite:前面项目如rqlite实现分布式的方式,是通过共识算法选举出某个Leader,之后由Leader对数据进行变更,实现了最终一致性。如果多个写入者同时对同一个数据库做修改而不发生冲突呢?我们知道实时协作领域中就存在一种解决多人实时协作的无冲突复制的数据结构:CRDT。而这个cr-sqlite项目就巧妙的通过SQLite的运行时扩展把CRDT带入了SQLite,最终实现了和mvsqlite一样的集群多节点并发写入特性。
推荐阅读作者的这篇文章:Why SQLite? Why Now?
Bedrock:Bedrock是一个建立在SQLite之上的网络和分布式事务层。它是为异地复制而设计的分布式关系数据库管理系统。采用P2P的分布式架构风格,数据最终被写入到所有节点的私有区块链中。🤯
Bedrock使用Paxos分布式共识算法选举集群Leader,Leader负责协调分布式两阶段提交事务。
Bedrock的同步引擎是一个私有区块链。每个线程都有一个叫做journal的内部表,它有3个列,叫做id、query、hash。每次查询被提交到数据库时,都会在日志中插入一条新行。新行记录了查询,并根据前一行计算新的增量哈希值。当一个服务器连接到一个集群时,最新的ID和哈希值被广播出来。如果两台服务器对ID对应的哈希值意见不一,那么它们就知道它们在某个时间点上 “分叉 “了,并停止彼此的通信。Leader决定哪个分叉能成为新的主分支。
总结
SQLite为什么能在如此多领域有创新的项目出现?一方面得益于SQLite高质量的代码,近亿行的测试代码保证了其坚若磐石的可靠性,另一方面又得益其简单的架构,总共的实现源码也才15万行。
对比其他数据库如MySQL有超过四百万行的源码,Oracle有超过上千万行的源码,就连Postgres的源码也超过百万行了。正如你很难让大象跳舞一样,个人开发者也很难单打独斗去在这种规模的代码上做创新。而SQLite的架构简单,代码量少,使得它成为了一个非常好的实验平台,让开发者可以在SQLite的基础上做出各种各样的创新。
随着Serverless及运行在CDN上的Edge Computing的流行,SQLite这种轻量级的关系型数据库将会有更多的应用场景,也会有更多的创新出现。SQLite也是一个非常适合学习的数据库,它的源码非常简单,可以很好的帮助你理解数据库的原理。
更新时间: 24个月前 版本: 53254d7f6
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
Real-time Web应用开发新体验
走进Web3
分布式系统中的时间
分布式系统下的认证与授权
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
AI驱动开发:从Prompt到Product(直播)
1 May 2024 5:58 | 1316 字 | 3198 次阅读
chatGPT • Prompt Engineering • Pair Programming
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
背景
准备通过系列直播的方式,展示如何从零开始使用将Prompt转变为Product的全过程。在这个系列直播里,我会完全用GPT4来设计并开发产品,代码开源,过程直播。
在2022年发布的文章AI降临后,我一直在用AI驱动开发的方式开发产品,这种方式已经成为我的工作方式。
在2023年,我与ChatGPT结对编程,开发了一个myGPTReader的产品,在这个产品的开发过程中,我几乎没有单独写过代码,而是通过ChatGPT生成的代码来完成开发。这种方式在我看来是一种新的开发方式,我称之为AI驱动开发。
在2024年,我又尝试完全用ChatGPT开发策引产品一个复杂的功能:多策略服务,策引产品旧的策略服务是一个复杂的遗留系统,不仅功能复杂,而且扩展性差,所以我用GPT4完全重构了技术架构,并优化了业务流程,提高了系统的可维护性和可扩展性。我的角色是决策宏观的架构与设计,其他尽可能让GPT4来写代码,而且所有的过程都在一个会话里完成,持续几个月的开发,消耗的Token过百万。这个过程的体验我已经在i365社区分享过,详情请看:完全用GPT4开发复杂服务的体验。
直播内容
计划的产品
以下是系列直播计划开发的产品,但最终产出可能会有所变化,一切依最终开发进度而定。
LetterDrop(小型项目)
LetterDrop是一款由Cloudflare Workers驱动的安全高效的Newsletter管理服务,可轻松创建、分发和管理订阅。
Repo: https://github.com/i365dev/Let…
回放:YouTube LetterDrop Playlist
完成进度:▰▰▰▰▰▰▰▰▰▰ 100%
点对点通信产品(中小型项目)
这个产品的灵感来自于在开发策引产品时需要一个能在线支持客户的功能。经过对现有SaaS产品和开源项目的调研后,发现都不太符合需求,因此决定自行开发一个能支持此需求的产品。经过在i365社区讨论后,觉得将这个开发过程直播出来会很有意思。这个系列直播也是i365社区《AI驱动开发指南》系列的一部分。
完成进度:▱▱▱▱▱▱▱▱▱▱▱▱▱ 0%
信息订阅机器人(中大型项目)
打造多个信息订阅机器人,能帮助我自动追踪、自动总结多个Feed消息流。
完成进度:▱▱▱▱▱▱▱▱▱▱▱▱▱ 0%
范围
直播内容将涵盖以下方面:
产品功能设计
域名选择
技术架构设计
技术栈选择
工程搭建
代码编写、测试、部署和上线
LandingPage设计
SEO优化
这些内容在去年的i365社区《个人产品开发指南》中已经有所涉及,但是去年是以AI辅助开发为主,而今年我们希望完全由AI驱动开发,这在策引产品开发中已经成为一种常态。
时间
计划每周六早上9点开始直播,每次结束时间依开发进度而定,预计持续数月。
看点
完全使用GPT驱动开发的特点是尽可能地使用Prompt来引导GPT生成代码,我将负责测试和组装,尽可能地避免自己编写代码。此外,GPT的应用不仅局限于技术和代码,还将参与产品功能设计、业务分析等与产品相关的各个方面,以尽可能地探索AI的边界。
渠道
YouTube: BMPI
小红书: 6739032172
B站: 1820902713
回放
YouTube:BMPI
围观交流
微信群:目前群成员已经超过四百多人,要加群交流请添加i365的个人微信(improve365_cn)拉群,如果无法添加(微信限流),请发送邮件至:me@i365.tech。
电报群:bmpi_group
Discord群:i365 Tech
更新时间: 11个月前 版本: 614ca274f
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
我与ChatGPT结对编程的体验
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
Real-time Web应用开发新体验
31 October 2022 19:44 | 4344 字 | 4690 次阅读
分布式技术
Real-time Web • Elixir • Erlang/OTP • 分布式系统
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
Real-time Web是什么
Real-time带来的技术难题
单机长连接管理
分布式集群扩容
数据状态的难题
IM集群的例子
Elixir的解决之道
让一切Live起来
让分布式变得简单起来
Elixir版IM集群的例子
总结
Real-time Web是什么
Web正变的实时可交互(Real-time Interactive)起来,从早期的内容发布通知类应用,再到移动时代社交媒体的分享类应用,现在逐渐涌现出很多实时协作类的应用。
The evolving online user experience.
The evolving online user experience.
这种变迁的驱动是由底层技术发展而带来了的,进而提升了Web类应用的用户交互体验。比如Web应用前后端通信协议从单向的HTTP REST到双向的WebSockets的发展,带来了Web用户体验更快的更新速度与更强的交互能力。
按照更新与交互的差异,Real-time Web又可以分为如下两个类型。
实时更新类:
信息流(Feed):资讯类网站实时更新突发新闻,社交媒体Feed流;
通知消息:社区通知,如知乎的问题回答更新通知功能;
金融证券:实时更新股票价格数据;
实时交互类:
直播活动:观众可对直播内容实时发送评论、弹幕等;
位置共享应用:多个用户可在同一房间实时共享自己的地理位置;
IM群聊应用:多个用户可在同一房间使用文本、语音或视频来聊天,如WeChat或Whatsapp,也有开源的实现如Tinode;
虚拟空间:多个用户可在虚拟空间里办公或游戏,如在gather里可以创建一个虚拟团队,成员能在虚拟空间里远程办公(如视频会议与白板协作),甚至可以拥有个人的虚拟办公室;
实时协作:多个用户可在同一房间实时协作,如在线白板类应用excalidraw或原型设计工具figma;
在线游戏:多个用户可在同一房间玩实时对战游戏;
Real-time带来的技术难题
Real-time相比之前的HTTP推拉机制(GET/POST),对服务端来说,挑战从如何处理大量的短连接,变成了管理大量的长连接。
在各类编程语言多种并发模型的支持下,再加上一些内核调优(kernel tuning)配置,现在的后端服务可以用HTTP协议在单机里并发处理上百万的短连接请求,更多详见这篇文章:
Extreme HTTP Performance Tuning: 1.2M API req/s on a 4 vCPU EC2 Instance | talawah.io
单机长连接管理
虽然在短连接上,单机并发上百万不是非常复杂的事情,但在长连接的管理上,单机上百万还是存在一定的难度,尤其是不同编程语言在实现方面还存在较大的差异:
Websocket Performance Comparison
Real-time相比传统Web来说,除了连接从短变成了长,每个用户的交互时间也变得更长。如果一个用户至少用一个长连接,那单机的架构很快就成为了瓶颈,唯一的办法是通过集群扩容解决单机资源瓶颈的问题。
但集群分布式架构的引入可能会带来更复杂的架构问题,如集群的扩容、数据状态的管理等。
分布式集群扩容
在分布式架构中,无状态的服务,可以很容易通过水平扩容(Horizontal Scaling)来提高服务吞吐量(Throughput)。如果是有状态的话,可先采用垂直扩容(Vertical Scaling),比如提升单机的硬件性能的方式,如果还是无法达到系统要求,则通过数据分区(Partitioning)的方式将数据分散存储在多个节点中。
数据状态的难题
由于数据分区很容易产生更复杂的分布式难题,比如节点添加删除导致的数据分区再平衡问题、数据一致性问题、分布式事务与共识问题等。所以尽可能把运行时的服务与数据拆分开,把服务做成无状态的,数据通过流处理系统如一些中间件去处理。但引入中间件又会带来额外的维护开销,也会让架构变的更为复杂。
而且Real-time类型的应用架构一般是基于异步事件驱动,而非实时数据同步的方式去实现的。异步事件驱动的架构在可观测性(Observability)上更困难一些,比如要在系统运行时去查看某事件流引发的系统行为,很难通过有限的日志去观测系统所有组件的运行时状态。
IM集群的例子
以IM聊天应用Tinode为例,它的集群是这么设计的:
假设集群有S1、S2与S3三个节点,有A、B与C三个用户,A加入了聊天室(T1, T3),B加入了(T2, T4),C加入了(T3, T6)。S1节点处理聊天室(T1, T2),S2处理(T3, T4),S3处理(T5, T6)。用户的客户端可以接入集群任意一个节点的API Endpoint。此时的数据处理链路如下:
A(T1, T3) -> S1(T1, T2) .(forward). T3 -> S2
B(T2, T4) -> S2(T3, T4) .(forward). T2 -> S1
C(T3, T6) -> S3(T5, T6) .(forward). T3 -> S2
这里的.(forward).指节点无法处理此请求,将请求转发到合适(通过一个全局的路由映射表查找)的节点去处理。
假设此刻S2下线,S2处理的T3与T4需要分区再平衡到S1与S3节点上,在这里可以用一致性哈希算法(Consistent hashing)处理集群节点数据(这里指的是聊天室状态)再平衡的问题。节点一致性可以通过诸如Raft的共识算法达成共识。此时的数据处理链路如下:
A(T1, T3) -> S1(T1, T2, T4) .(forward). T3 -> S3
B(T2, T4) -x-> S2()
C(T3, T6) -> S3(T5, T6, T3)
当S2下线后,B的客户端检测连接断开后重新接入其他节点比如S1或S3,也可以在服务端部署HAProxy或Nginx去自动处理。由于A和C的T3聊天室再平衡到S3节点了,A和C的客户端需要重新加入到S3节点的T3聊天室。当S2节点重新上线时,需要再平衡至之前的状态,相应的客户端也需要重新接入。
这里的聊天室在实现时一般是在WebSocket上订阅的Channel名。
Elixir的解决之道
Real-time叠加分布式带来的难题让可交互的实时Web应用开发变的复杂起来。但基于Erlang/OTP的Elixir语言让这类应用的开发变的简单起来。
Elixir继承了Erlang/OTP的很多特性:天然分布式、容错、低延迟等。关于Elixir的进一步介绍可以参考我之前写的一个Slide:Elixir介绍。
Elixir是如何解决Real-time Web应用开发的痛点?下面从两个方面来介绍。
让一切Live起来
得益于Erlang/OTP出色的用户线程模型,Elixir的Web框架Phoenix可以轻松在单机处理上百万的长连接:
The Road to 2 Million Websocket Connections in Phoenix - Phoenix Blog
为了发挥如此出色的长连接管理能力,Phoenix框架更是将Server-driven UI发挥到了极致:
JS driven UI vs LiveView driven UI
JS driven UI vs LiveView driven UI
如图所示,左侧是常规的Web页面渲染机制,数据通过网络从服务端拉取,前端通过数据控制页面组件的渲染。而Phoenix的LiveView可以让后端开发者摆脱写Javascript的困扰,所有的逻辑都在服务端完成,页面的渲染也是通过服务端计算页面更新的差异,将小部分差异通过WebSocket推送给浏览器。
Elixir让一切Live起来!
让分布式变得简单起来
除了优异的长连接管理能力,得益于Erlang/OTP天然分布式的能力,相比流行的集群技术如K8S(也叫容器编排),Elixir组建集群更是非常的简单。简单到不需要过多赘述,发布一个多节点的集群只需要几分钟的时间,具体可参考Fly.io这篇配置文档:
Clustering Your Application · Fly Docs
Elixir的集群能做到非常细粒度的控制,比如在节点上创建一组Process(此进程并非操作系统级别的进程,而是Erlang VM管理的轻量级用户线程),可以将其通过一致性哈希分布到其他节点的Erlang VM上,目前这种操作在其他编程语言(非Erlang/OTP类)上是无法实现的。具体可参考这些项目的介绍:
derekkraan/horde: Horde is a distributed Supervisor and Registry backed by DeltaCrdt
NetComposer/nkdist: Erlang distributed registration and load balancing
Elixir另外一个让分布式集群管理变得简单的能力是其强大的可观测能力。如下是一个集群的Phoenix LiveDashboard:
LiveDashboard可以查看集群的很多信息,比如我们可以查看应用启动后的监督树(Supervisor tree),这是Elixir强大的容错能力来源,如果某个Process出现了Crash的情况,监督树会自动恢复该Process,这也是Erlang/OTP的Let It Crash思想。
进一步我们可以查看某个Process的状态,比如此Process的PID为<0.2243.0>,第一位0表示这是一个本地进程,如果是非0,则是一个远程节点的Process。
当然目前这些还不算啥惊人的部分。惊人的地方在于我们可以与这些Process在线实时交互,比如获取它们当前的状态,给它们发消息获取处理后的状态,这些在排查运行时问题时非常有帮助。比如我远程接入到集群某个节点的Erlang VM中:
如上图,我在接入此节点后,可以与此Process通信并修改它的状态,甚至能找到某个负责与Web端通信的WebSocket的Process,通过此Process可以直接给Web端推送数据。
你可能注意到了上面两个图中Process的PID并不相同,第一个是<42700.16451.14>,第二个是<0.16451.14>,这是因为前者是我在另外一个节点上查看此节点的Process,当然就是一个remote的Process了,后者是我接入了此Process所在的节点。一旦组建好集群,集群内的节点都能很容易获取其他节点的运行时数据。
Elixir版IM集群的例子
上面介绍了Tinode的集群设计,因为Tinode是用Go开发的,它的集群是自己实现的。但用Elixir就不需要这么复杂了,free4chat是我开发的一个开源的基于WebRTC的语音聊天室Web应用,它是基于Elixir开发的,目前服务端是两个节点的集群,上面的截图就是来自它的Elixir后端集群。
free4chat的集群实现非常简单,利用Elixir的强大的集群管理能力,当某个用户通过某个节点接入到某个房间后,该节点从集群全局注册表中查找是否存在该房间的Process,如果不存在就创建,存在就获取该房间的Process,无论该Process是否在本地,用户都可以在该房间与其他人聊天,不同节点Process间的通信由Erlang/OTP自动实现。
总结
Real-time Web开发本身不复杂,复杂的是Real-time叠加分布式带来的难题,当然难题一般只存在同一维度上,当脱离该纬度进入更高维度时,这些难题也许就被轻松解决了。
分布式领域有非常复杂的经典问题,Erlang/OTP在该领域深耕了很多年,很多都有了工业级的解决方案,站在Erlang/OTP巨人肩膀上的Elixir不仅继承了其强大的分布式解决方案,还在多个领域开辟了新的开发体验,比如Phoenix LiveView另辟蹊径带来了新的Web开发体验。
本文没有提到的是,上述的例子并没有考虑数据库的问题,如果再引入传统的数据库,会让问题变得更为复杂。数据库如何能给Realtime Web带来新的开发体验?也许这两篇文章能给我们更多启发:
Advancing the realtime web - RethinkDB
SurrealDB | Why use SurrealDB?
更新时间: 31个月前 版本: 40cf03f99
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
SQLite的文艺复兴
走进Web3
分布式系统中的时间
分布式系统下的认证与授权
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
Google软件工程之工具篇
16 September 2022 20:31 | 4516 字 | 7936 次阅读
软件工程
软件工程 • 分支管理 • 构建系统 • 代码搜索 • 静态分析 • 依赖管理 • 持续集成 • 持续交付
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
软件工程还是软件设计
Google软件工程中的工具
版本控制(Version control)
CVCS vs DVCS
单一代码仓(Monorepo)
分支管理(Branch management)
代码搜索、构建与静态分析(Code search && Build system && Static analysis)
依赖管理(Dependency management)
持续集成与持续交付(Continuous Integration && Continuous Delivery)
软件工程还是软件设计
传统工程的流程比如土木工程是设计师先设计好图纸,然后工程队按照设计图纸去施工建造,所以这里的工程既包含设计又包含建造,但负责设计的人员明显与建造的人员不是同一类人,甚至有着非常大的差异。
那软件的生产流程是什么呢?以敏捷开发流程为例,组建一个软件开发队伍,先进行Inception确定好开发的需求及范围,之后根据需求拆分故事卡,开发人员根据故事卡实现产品需求。在实现故事卡的过程中,开发人员每天会写一部分代码并在本地做自测,之后会对代码做Code Diff,在这个过程中又可能重新修改设计与实现。不断重复这个过程,直到最终这部分代码进入集成环境被测试人员验收,最终会上线到生产环境。那么这个过程中既包含了设计又包含了实现(或者说建造),或者说这实际上是个不断设计的过程。
以下两篇文章推荐阅读,可能会让你对这个问题有更好的理解:
Are We Really Engineers?
What Is Software Design?
Google软件工程中的工具
以下是《Software Engineering at Google》一书第四部分工具篇的思维导图,由于此部分占全书近40%,所以本文不会详细地介绍其中的概念,想详细了解的读者建议阅读原书。本文会结合此书这部分内容分享作者的个人理解及相关经验。
Live at Head
绑定分发模式
语义化版本
静态依赖模型
运营
许可证
安全性
兼容性
钻石依赖
Nx
Gradle
Bazel
Make
Ant
Scaled Trunk-Based Development
Trunk-Based Development
GitHub Flow
Git Flow
Polyrepo
Monorepo
分布式(Distributed VCS)
集中式(Centralized VCS)
管理方式
问题
基于制品的构建系统
基于任务的构建系统
分支管理
存储类型
分类
持续交付
持续集成
依赖管理
静态分析
构建系统
代码搜索
版本控制
工具(Tool)
过程(Process)
文化(Culture)
Google软件工程
版本控制(Version control)
在众多软件工程所用的工具中,最重要的我觉得就是版本控制系统了(Version Control System)。版本控制系统从字面意思就可以看出来是控制源代码的版本的,VCS就像时间宝石一样让开发人员在源代码历史中穿梭,为什么这种能力很重要?
其实这和本文开头那个问题相关,如果说软件开发是一个设计的过程,那这个设计可能需要不断修改,能最低成本地在不同版本间切换非常重要,更重要的是这种能力可以让多人协作完成软件的设计与开发。
Development is inherently a branch-and-merge process, both when coordinating between multiple developers or a single developer at different points in time. (Software Engineering at Google)
版本控制也让软件开发过程中产生了Code Diff或Code Review的过程进而促进团队知识共享,而这又是软件工程中文化的一部分。版本控制也影响了软件的部署过程,比如结合Pipeline与Artifact Repository,可以构建出不同环境不同版本的软件制品。
CVCS vs DVCS
早期的版本控制系统是集中式(CVCS)的,比如Subversion,现在更流行的是分布式的(DVCS),比如Git。这两者的区别可以看这篇文章:
GitSvnComparison
CVCS与DVCS仅仅是适用的场景不同,并不意味着后者是前者更好的替代。比如很多大的公司或组织,如Google、Microsoft与FreeBSD都在用CVCS。一般来说大的公司更偏向于用CVCS,与CVCS密切相关的就是单一代码仓(Monorepo)了。
分布式版本控制系统如Git,其实是没有中央存储库的。我们在GitHub克隆某个仓库到本地,其中的origin其实是刻意约定设置成中央仓库的,但我们可以在本地仓库中添加多个远端中央仓库,也可以rebase多个远端仓库的代码到本地仓库。
单一代码仓(Monorepo)
Monorepo简单理解就是把整个组织的所有项目的代码都放入一个仓库中。初看不可思议,但Monorepo并不仅仅是把代码放一块就行了,它需要一整套的流程与工具链支撑,比如不同团队协作模式、代码库之间的依赖管理、目录的权限配置、构建与发布等。
与以Git为主的Polyrepo(一个项目一个代码存储库)存储库模型相比,Monorepo有如下的好处:
代码共享:所有人都可以看到其他人的代码,能降低重复代码;
统一依赖:不会出现多个项目依赖相同三方包的不同版本导致的冲突问题;
跨项目修改简单:大规模跨项目的重构更简单了,能一次修改多个项目的代码;
共享构建发布流程:能共享同一套构建发布流程,简化基础设施的复杂性;
Developers within an organization must not have a choice where to commit, or which version of an existing component to depend upon. (Software Engineering at Google)
进一步了解,强烈推荐阅读这篇文档:
Monorepo Explained
分支管理(Branch management)
版本控制系统不仅可以让开发人员具备时间穿梭的能力,还具备开辟多重宇宙的能力,这就是分支(Branch)的功能。分支不仅仅是代码的不同版本,它还深刻的影响了开发部署的流程。
早期流行复杂的Git Flow分支模型,但这种模型带来了很复杂的维护成本,包括分支的管理、冲突的解决等问题。最终逐渐演变出更简单的主干分支开发(Truck Based Development)模型。
主干开发分支在实践中可能存在的问题是,主干分支与流水线(Pipeline)的集成,一般会有不同环境,如CI、INT、UAT、PROD等。当开发人员要在集成环境测试时,如果有紧急的Hotfix代码要推送到生产环境,这时候主干分支中还包含着集成环境的开发代码,就算有特性开关(feature toggle)的支持,也不敢直接把这些代码推入到生产环境。此时能做到就是回滚(git revert)这部分代码回去。这个问题本质还是因为测试环境有限,无法做到一个代码变更部署到一个临时创建的测试环境中,这时候主干开发分支可能需要做一定的调整,比如用Release分支来发布,主干分支做开发代码的Single Source of Truth。
不同分支模型的介绍,推荐这篇文章:
Git(Hub) Flow, Trunk Based Development, and Code reviews
代码搜索、构建与静态分析(Code search && Build system && Static analysis)
代码搜索可以用最简单的grep -r命令或者IDE的搜索功能来实现,但要在多个代码仓库间高效地对某些代码进行跨仓库搜索,那这些工具可能很难满足需求。
Google自研了一套代码搜索的工具,这个代码搜索工具甚至可以和其他系统如日志查看系统集成。
Sourcegraph是一个开源免费的代码搜索云服务,可以与GitHub集成,提供良好的代码阅读体验。
Google同样实现了自己的基于制品的构建工具Bazel,Bazel也是支持Monorepo很好的构建工具,同样的还有Nx与Gradle。
代码静态分析就像自动化的Code Review一样,能帮助发现代码中的质量与安全问题,减少不必要的Review时间,提升代码质量。流行的代码静态分析工具中,SonarQube是推荐的。
依赖管理(Dependency management)
依赖管理可能是软件工程中最复杂的问题之一(短期编程代码无需考虑此问题)。现代软件是建立在大量的依赖库或框架之上的,这些外部代码很多并不受开发人员的控制,当软件变得越来越庞大时,大量的依赖可能会形成复杂的依赖树(如在Gradle项目中,gradle dependencies命令可以打印出应用的依赖树)。
依赖问题最多的可能是钻石依赖问题,简单说就是同一个包的不同版本共存的问题,这在某些编程语言如Java中影响并不大,因为多个版本可以共存,除非在某些特殊的场景下,不同的包可能会造成一些很诡异的Bug。
在Black Duck中又把依赖的问题分为三大类:
许可证(License Risk):商业应用对依赖包的License有限制,比如无法使用GPL类的License。
安全(Security Risk):依赖包经常会被爆出重大的安全CVE问题,有时候因兼容性的问题很难去通过版本升级来修复。
运营(Operational Risk):一些小众的编程语言如Clojure的很多包,经常无人维护或者缺乏更新,导致存在潜在的运营风险。
另外一个主要的问题就是兼容性的问题,比如API出现破坏性的更新,或者ABI无法兼容。
编程语言ABI(Application binary interface)兼容性:与API(Application programming interface)类似,是描述二进制文件的兼容性。比如Java有ABI兼容性的保证,意味着基于新版本JDK的代码可以安全地调用老版本JDK的Jar包。
在解决API变化导致的依赖问题上,业界一个流行的方案是语义化版本:SemVer。通过将版本拆分为三部分,如x.y.z,x是破坏性更新版本号,y是特性版本号,z是Bug修复的版本号。我们可以在依赖配置文件如package.json中通过^或~符号来指定依赖的最大版本号范围。
依赖管理的问题也可能和代码设计有关。比如应用对某个外部服务有依赖,如何降低外部API变化对应用代码的影响?这个问题可以从设计模式的角度去解决,比如创建一个适配层(如Gateway模式),通过定义一个抽象的接口层去实现,而非依赖具体的外部API去实现。
持续集成与持续交付(Continuous Integration && Continuous Delivery)
CI是一种团队开发软件的实践,在代码变更集成到主代码分支前尽早的捕捉变更带来的问题,流程主要有自动化的测试与构建,CI工具可以帮助开发人员快速获得代码变更是否正确的反馈。
常用的CI工具有:GitHub Actions,GoCD与Jekins。这些工具也称为流水线(Pipeline),不仅支持UI的操作,还支持Pipeline as Code。
实际的CI工具一般受制于服务器资源的限制,很难做到一个代码变更(Code Commit)自动部署一个测试验证环境(这也被称为无限环境CI)。目前只有少数的云服务可以支持前端项目的无限环境CI,比如Cloudflare Pages,Vercel与Netlify等。
CD发生在代码集成后,包括从代码集成后到发布变更的软件给用户的过程,良好的CD实践既可以快速进行价值交付,又可以快速获得用户反馈。持续交付的原则和敏捷的方法论有一些重合的部分:
敏捷:小而频繁地发布过程,快速获取反馈。
自动化:通过自动化的手段降低发布的时间成本。
隔离:采用模块化的架构设计使需求变更和故障排除更简单。
可靠:通过技术监控提高系统的可靠性。
数据驱动:使用埋点或A/B测试获取用户反馈的数据,通过数据做决策。
分步发布:产品特性先灰度发布,确保无误后再全量推送给用户。
从这个角度看,《Google软件工程》这本书只是从大纲的角度去介绍这些知识,真正的问题还需要我们在研发软件的实践中去体会。这系列文章也只是结合作者自己的知识经验去理解这本书,如有错误,欢迎指正。
更新时间: 33个月前 版本: 3cfd2d5dc
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
Google软件工程之过程篇
Google软件工程之文化篇
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
Google软件工程之过程篇
8 August 2022 35:53 | 7896 字 | 7953 次阅读
软件工程
软件工程 • Style Guide • Code Review • Technical Documentation • Unit Test • Test Pyramid • Deprecation
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
风格指南(Style Guide)
代码评审(Code Review)
Code Review v.s. Code Diff
技术文档(Technical Documentation)
测试(Testing)
单元测试(Unit Testing)
测试替身(Test Doubles)
较大型的测试(Larger Testing)
弃用(Deprecation)
总结
上篇介绍了Google软件工程中的文化部分,本篇介绍软件工程中主要的过程部分,包括编码风格指南、代码评审、技术文档、自动化测试(单元测试、集成测试与较大型测试)与弃用。
以下是《Software Engineering at Google》一书第三部分过程篇的思维导图,由于此部分占全书近40%,所以本文不会详细的介绍其中的概念,想详细了解的读者建议阅读原书。本文会结合此书这部分内容分享作者的个人理解及相关经验。
测试模型的选择
反模式
金字塔模型
缓慢的测试
脆弱的测试
考虑被测试的行为
覆盖率目标是底线
仅测量单元测试覆盖率
测什么
测试原则:测试所有不想被破坏的东西
实践
广范围测试
中范围测试
窄范围测试
实践
大
中
小
Testing
Code Search
How/By who
alert fatigue
hope is not a strategy
功能无用、重复或被替代
维护成本太高
交互测试(Interaction Testing)
打桩(Stubbing)
伪造(Faking)
真实实现
交互测试(Interaction Testing)
验证函数调用行为及参数
打桩(Stubbing)
赋予函数行为的过程
伪造(Faking)
API的轻量级实现
Mocking框架
缝(Seams)
DAMP原则
测试不应包含逻辑
测试名称应提现测试行为
测试行为而非方法
不清晰的测试
脆弱的测试
无法取代人工探索性测试
在厕所推广测试(TotT)
测试认证计划
针对新员工的专项课程
测试套件的陷阱
关于测试覆盖率
The Beyoncé Rule
测试范围
测试大小
自动化测试让持续交付变的更容易
好的测试反向提升代码设计
让代码评审更简单
测试代码是更好的文档
提升对代码变更的信心
更少的Debugging
工具
里程碑
Owner
警告
强制
咨询
谨慎启动新项目
演进而非弃用
被弃用的,和没有准备好的
系统启用,代码保留
系统用户越多,越难弃用
代码越少,功能越多
旧不意味着过时
过时的系统需要弃用
代码是负债而非资产
因为需要维护
工作流
类型
结构
挑战
为什么需要
何时使用
测试替身技术
基本概念
好的实践
提高可维护性
优势
自动化测试的局限
Google测试文化的历史
设计测试套件
测试代码的好处
弃用文档
好文档的特征
三段式
5W+1H
登陆页(Landing Page)
概念类(Conceptual)
教程类(Tutorial)
设计类(Design)
引用类(Reference)
文档评审
了解受众
文档流程化
文档即代码
重构
Bug Fixes
优化代码
特性代码
金字塔模型
尽可能自动化
小规模评审
清晰的变更描述
小的变更
友善且专业
塑造团队工程文化
促进团队知识共享
代码一致性
代码可读性
代码正确性
流程
类型
在设计时考虑弃用
困难
为什么
较大型的测试
测试替身(Test Doubles)
单元测试(Unit Testing)
测试概述
文档写作最佳实践
文档类型
一些实践
评审类型
最佳实践(Best Practices)
好处
Deprecation
Testing
Technical Documentation
Code Review
Style Guide
工具(Tool)
过程(Process)
文化(Culture)
Google软件工程
风格指南(Style Guide)
We value “simple to read" over “simple to write.” (Software Engineering at Google - Style Guides and Rules)
代码可能只会被写一次,但会被读很多次。如果团队成员的代码风格都不统一,可读性会很差,所以保持团队代码风格统一很重要。
历史证明,能写的很飘逸的编程语言使用人数一般都不会很多,典型的如古老的Perl语言,可以达到“一人千面”的代码风格。而写起来中规中矩甚至没有啥高级技巧的语言如Java、Go等在工业上反而用的很多。
Google的代码风格指南不太适合一般规模的公司,所以此部分不做过多介绍。从我的个人经验来说,一般项目上会配置一套自动化的代码风格检查工具(如checkstyle),甚至会集成到流水线(Pipeline)中强制团队保持一致的代码风格。某些编程语言如Go在构建工具中也提供了gofmt的代码格式化工具。
代码风格指南只能解决一些很基本的可读性问题,如代码缩进、函数命名风格、代码行数限制等。但代码的可读性可不只体现在这些表面,更深层次的可读性问题如API语义的可读性该怎么解决?一个可行的实践是代码评审。
代码评审(Code Review)
代码评审是如此重要,以至于其在Google是必须做的一个实践过程。它能提供以下的好处:
代码正确性:评审人员可能发现评审代码中的逻辑问题,从而提前消除一些潜在的Bug;
代码可读性:代码能否被其他人很容易的理解?API语义设计是否合理?是否包含测试?是否有必要的文档与注释?
代码一致性:代码风格是否与团队和组织保持一致?
促进团队知识共享:代码评审可以让团队其他成员了解你所做工作的上下文;
塑造团队工程文化:团队保持代码评审的实践,本身也是团队工程文化的一部分,能让新的成员迅速适应团队工程文化;
代码评审的最佳实践有以下:
友善且专业
小的变更
清晰的变更描述
小规模评审
尽可能自动化
金字塔模型
代码评审金字塔模型如下图所示:
图片来源:《The Code Review Pyramid - Gunnar Morling》
图片来源:《The Code Review Pyramid - Gunnar Morling》
代码评审的反模式是倒金字塔模型,也就是很多时间花费在了可以自动化执行的部分比如代码风格的统一、自动化测试等,但在金字塔模型里,代码评审应该把主要的精力放在API语义、实现语义及文档等部分。
Code Review v.s. Code Diff
Diff和Review的区别在于前者是一个团队集体行动,团队成员一块看某个开发者前一天写的代码,这样的好处在于每个人都能反馈,也能了解其他人做的工作,防止一些信息不同步的问题。代码评审一般是一两个人(可能甚至是团队外部的人)去审查对方要合入主干分支的代码,更适合外部人员提交代码到主干这种GitHub PR分支管理模式。
我所在项目的团队每天会做Code Diff ,这是个必须的实践。团队规模在几人以内可以让每个人都有时间讲解自己的代码,如果代码太多,那可以给每个人一个时间限制。如果团队太大那可以拆分成多个stream来管理,总之Diff的人员不能太多,但每天都应该花时间做,因为收益要高于成本,可以统一代码风格,保证可读性,提高成员技术水平。
技术文档(Technical Documentation)
清晰
准确
完整
弃用文档
好文档的特征
三段式
5W+1H
登陆页(Landing Page)
概念类(Conceptual)
教程类(Tutorial)
设计类(Design)
引用类(Reference)
文档评审
了解受众
文档流程化
文档即代码
文档写作最佳实践
文档类型
一些实践
Technical Documentation
技术文档与代码一样应该得到开发者同等的重视,但有太多文档与代码不同步的场景出现,导致文档的可用性大大降低。为什么会出现这种问题?一方面是因为开发者重视度不够的问题,另外一方面是因为写一份好的技术文档并不是一件简单的事情。
如何写一份好的技术文档?推荐阅读如下的文章:
Technical Writing for Developers
Technical Writing | Google Developers
SEO Copywriting Guide
开发人员不喜欢文档的另外一个原因在于,代码和文档的工作流程并不相同,一般文档都存放在与代码不同的位置,比如某个FTP目录以Word的格式存在。要是文档的编写可以和代码在同一套工作流里,就能极大的降低开发者的心智负担,这正是Docs-as-code的设计理念,具体的流程实践可以看这篇文章:
Working in public — our docs-as-code approach
测试(Testing)
产品信心
工程生产力
沙漏模型
冰激凌甜筒模型
5%E2E测试
15%集成测试
80%单元测试
同步等待其他依赖
启动系统
处理大数据集
滥用Mock
糟糕的测试代码
测试稳定可靠吗
对依赖的破坏性更新有信心
对系统的正常工作有信心
异常与错误
安全
可用性
行为正确性
性能
测试模型的选择
反模式
金字塔模型
类比E2E测试
验证系统间交互
类比单元测试
验证系统组件交互
类比单元测试
验证代码逻辑
清晰简单的测试
测试之间应隔离
类比E2E测试
在构建和发布时执行此类测试
主要验证环境配置
可跨机器
类比集成测试
可跨进程但不能跨机器
类比单元测试
无法执行阻塞线程的操作
(测试替身)
单线程
缓慢的测试
脆弱的测试
考虑被测试的行为
覆盖率目标是底线
仅测量单元测试覆盖率
测什么
测试原则:测试所有不想被破坏的东西
实践
广范围测试
中范围测试
窄范围测试
实践
大
中
小
无法取代人工探索性测试
在厕所推广测试(TotT)
测试认证计划
针对新员工的专项课程
测试套件的陷阱
关于测试覆盖率
The Beyoncé Rule
测试范围
测试大小
自动化测试让持续交付变的更容易
好的测试反向提升代码设计
让代码评审更简单
测试代码是更好的文档
提升对代码变更的信心
更少的Debugging
自动化测试的局限
Google测试文化的历史
设计测试套件
测试代码的好处
测试概述
测试是软件工程过程中很重要的一个组成部分,而这里的测试主要指自动化测试过程,人工测试占比很少。测试也有一个金字塔模型,如下图所示:
关于测试金字塔的细节,推荐阅读这篇文章:
The Practical Test Pyramid
开发人员写自动化测试有如下好处:
更少的Debugging:有了自动化测试后,系统的很多行为可以通过测试代码观察到。当然Bug一旦产生说明测试代码覆盖不全面,需要补上相关的测试,久而久之,测试代码就形成了非常全面的防护网。
提升对代码变更的信心:当有了测试防护网后,对代码一旦产生破坏性的更新,测试代码会失败,这就给开发人员机会在部署前去修复此问题。
测试代码是更好的文档:当面对一个完全陌生的代码库时,除了有限的文档,另外一个了解系统行为的方式就是看测试代码。测试代码相比文档,有着更全面清晰的业务细节,能给予开发人员更多的信息去了解此业务系统。
让代码评审更简单:测试代码相比生产代码更接近业务视角,能让评审人员从业务系统对外行为的视角去了解生产代码的意图。这样也能让评审人员做出更有效的反馈意见。
好的测试反向提升代码设计:要让系统模块具备一定的测试性才能写出测试代码,所以有测试的代码从设计的角度看,其可读性与解耦度相比没有测试代码的要更高。敏捷实践中推崇的TDD(Test Driven Development)就是一种通过测试驱动出好的实现代码的实践。
自动化测试让持续交付变的更容易:如果没有自动化测试的帮助,代码部署上去后出Bug的概率要更高,这会提高系统交付的时间。
没有测试代码的系统是遗留系统。
单元测试(Unit Testing)
通用支持类需有独立测试
只测试公开的接口
局部支持类无需独立测试
不包含无关的信息
通过冗余提供完整的信息
合适的测试范围
避免测试具体实现
业务行为改变
(只有在此情况下才修改测试)
Bug Fixes
(不应修改测试)
新特性
(不应修改测试)
重构
(不应修改测试)
适当冗余而非精简
测试代码简单直接
结果预期
测试行动
then
when
given
编写完整简洁的测试
测试状态而非交互
测试公开接口
不变的测试
DAMP原则
Descriptive And Meaningful Phrases
当在测试代码共享代码和数据时采用此规则
测试不应包含逻辑
测试名称应提现测试行为
测试行为而非方法
不清晰的测试
脆弱的测试
能作为技术文档
失败时能快速定位问题
能提高测试覆盖率
容易编写
小且快,能立即获得反馈
好的实践
提高可维护性
优势
单元测试(Unit Testing)
单元测试作为占比测试金字塔最大部分的底座,重要性不言而喻。它的优势很多,但Google在多年的实践中发现,提高单元测试的可维护性非常重要。而难以维护的测试代码主要有两方面造成:
测试脆弱:当在代码重构、添加新特性及修复Bug时,会出现一些测试无法跑通,只能通过修改测试的方式来解决,这说明已有的测试很脆弱。好的测试应该只有在系统的业务行为发生改变时,才需要修改生产代码和测试代码。造成测试脆弱的原因有很多种,可能的原因包括测试隔离没做好,比如依赖了很多共享的全局性状态,或者测试了非公开的函数或方法,又或者测试的粒度过细,把很多实现细节给测试了。
测试不清晰:不清晰的原因也有很多方面,比如测试的名称并没有体现其测试意图,在单个测试中测试了一些不必要的行为,又或包含了很多无关的信息。
要提高可维护性,一些好的实践包括以下方面:
测试行为而非方法:很多测试框架如Junit都倡导Given/When/Then三段式测试编写方式,这样可以从验收标准(Acceptance Criteria)的业务视角去编写测试,而非针对单个函数或方法去编写测试(这很容易写出脆弱的测试)。
测试名称应提现测试行为:当单元测试失败时,最先看到的就是测试失败单元的名称,好的测试名称能以最直接的方式体现该测试意图,所以测试名称长一些也可以。
测试不应包含逻辑:因为测试单元本身并没有额外的测试,如果测试包含了比较复杂的逻辑,可能会导致测试代码的Bug。所以测试代码中尽可能不包含逻辑计算的过程。
DAMP(Descriptive And Meaningful Phrases)原则:在生产代码上业界倡导DRY(Don’t repeat yourself)的基本原则。而在测试代码中,正如上面几条实践表明,一定程度上的代码冗余是有必要的,这能帮助我们编写出简单而清晰的测试代码。
单元测试的代码执行速度一定要快,但在要测试的生产代码中,可能包含了执行速度很慢的代码,比如网络或文件等I/O操作,又或者对数据库的请求,甚至需要整个应用启动来获得完整的执行环境。如何将这类慢的代码与真正要测试业务逻辑的代码隔离开来?那就是接下来要介绍的测试替身技术。
测试替身(Test Doubles)
可能让测试变得低效
难以测试有副作用的状态
可能让测试变得脆弱
测试代码包含了实现细节
可能会让测试意图不清晰
不要过度测试
仅测试必须测试的信息
需测试函数调用次序时使用
需测试函数副作用时使用
容易让测试变得脆弱
测试代码包含了实现细节
尽可能测试状态而非交互
少量函数仅依赖返回值时
在真实实现和伪造不可用时
谨慎使用打桩
需要写测试
考虑伪造的保真度
当真实实现不可用时
权衡维护成本与收益
实例构造简单
确定性的
执行时间快
依赖注入(Dependency Injection)
交互测试(Interaction Testing)
打桩(Stubbing)
伪造(Faking)
真实实现
交互测试(Interaction Testing)
验证函数调用行为及参数
打桩(Stubbing)
赋予函数行为的过程
伪造(Faking)
API的轻量级实现
Mocking框架
缝(Seams)
通过使用测试替身
实现可测试性的技术
何时使用
测试替身技术
基本概念
测试替身(Test Doubles)
测试替身能通过一些模拟或伪造的技术来控制被测试代码的执行路径,比如在OOP中我们可以通过接口的多个实现,来完成生产代码与测试代码的不同实现。
由于测试替身技术本身非常成熟,所以本文不做基本的介绍,推荐阅读这篇文章进一步了解:
TestDouble
在Google的多年实践中发现,测试替身很容易被滥用,造成很多脆弱的测试,而被滥用最多的就是打桩(Stubbing)技术。不同替身技术都有其适用场景,推荐的一个决策流程是:
如果生产代码的执行时间足够快,那就不需要替身技术,直接测试生产代码;
如果伪造(Faking)的实现成本很低,且伪造的保真度够高(能尽可能模拟真实的使用场景),则推荐使用伪造替身技术;
如果在前两者都不可用的情况下,仅被测试代码只依赖少量函数或方法的返回值时,可以使用打桩(Stubbing)替身技术;
交互测试(Interaction Testing)替身技术谨慎使用,如果要用也仅在需测试函数副作用或调用次序时使用,并且不要过度测试不必要的数据;
较大型的测试(Larger Testing)
提供支持和联系信息
尽可能减少定位问题的成本
如日志打印分布式追踪ID而非堆栈信息
清晰的故障定位信息
密封的SUT环境
优化测试构建时
降低内部系统超时和延迟
使测试易于理解
驱除松散性
加速测试
Bug Bash
断言
手工
真实基线
手工数据
采样数据
测试流量
种子数据
契约测试(Contract test)
单元测试覆盖不到的位置
意外的修改
维护
运行
编写
混沌测试(Chaos)
用户验收测试(UAT)
A/B测试
探索性测试
部署配置测试
性能测试
浏览器和设备测试
功能性测试
验证
测试数据
SUT
缺乏开发标准
维护与归属的问题
紧急行为和真空效应
预期外的行为与副作用
负载下的问题
环境配置的问题
单元测试保真度的问题
工作流
类型
结构
挑战
为什么需要
较大型的测试
在测试金字塔的顶端是占比只有20%的集成测试与E2E测试,虽然占比少,但其却可解决单元测试的以下问题:
保真度的问题:单元测试因使用测试替身来加速执行时间,但替身与实现本身就存在保真度的问题,一旦被替身的实现发生改变,单元测试因模拟行为未变,可能造成一些意想不到的Bugs。
环境配置的问题:环境的问题只能在接近生产环境的测试环境(如UAT)环境中去测试与发现问题,这是单元测试无法覆盖的测试范围。如Google的一些重大全球性的Bug都和环境配置问题有关系。
负载下的问题:在压力测试下,系统的行为表现如何?性能是否能达到业务要求?这类非功能性的需求测试只能在E2E测试中完成。
预期外的行为与副作用:单元测试是在开发者预期的视角下完成的,所以存在一定的视角盲区。在一个接近生产环境的测试环境测试是发现这类问题最好的办法。
紧急行为和真空效应:如果系统的运行时环境发生一些意外的修改,如集群网络配置或部署配置发生变更,这类问题也只能在集成环境中发现。
较大型测试的编写与维护都是成本高昂的,在我们项目实践中,一般和业务系统强相关的集成测试和部分E2E测试都是业务开发团队完成的。但一些公共的E2E测试,比如某个全局性的功能性测试,可能由一个独立的小组完成,也可能只完成一个MVP的版本,之后由业务系统维护团队开发完成。
推荐进一步阅读的文章:
浅谈契约测试
契约测试之核心解惑
弃用(Deprecation)
Testing
Code Search
How/By who
alert fatigue
hope is not a strategy
功能无用、重复或被替代
维护成本太高
工具
里程碑
Owner
警告
强制
咨询
谨慎启动新项目
演进而非弃用
被弃用的,和没有准备好的
系统启用,代码保留
系统用户越多,越难弃用
代码越少,功能越多
旧不意味着过时
过时的系统需要弃用
代码是负债而非资产
因为需要维护
流程
类型
在设计时考虑弃用
困难
为什么
Deprecation
代码是资产还是负债?Google的答案是负债,因为代码需要不断的维护才能正常工作。负债是有高昂的利息,降低负债最好的办法就是在不需要的时候砍掉它。而这就是弃用过程的价值。
对开发人员来说,弃用是个难以接受的过程,因为幸苦写的代码,很难下定决心去销毁它。所以一个中庸之道是在代码将要被弃用前,想办法通过演进的方式给予其二次生命。如果非要弃用,也只是停止维护和运行,旧的代码依旧会在代码仓库中可被搜索到,历史记录也会被保留。
我的个人项目实践是,下线一个系统是一件需要重视的过程。一个系统一旦被发布,它被使用的场景就很难以想象,API的用户可能会以意想不到的方式去使用它。所以尽可能通过代码搜索去找到其被使用的场景,之后再给充足的Deadline广而告之,甚至可以主动与用户沟通,确保不会让其出现大的损失。
总结
代码可能只会被写一次,但会被读很多次。所以软件工程中的过程部分主要致力于解决代码可读性的问题。无论是风格指南、代码评审、文档甚至自动化测试,很大程度上都在为提高代码可读性。
写代码很容易,能写出易懂的代码却有难度。所以从这个角度看,写代码是个入门简单精通却难的技能,需要我们不断的精进,通过多种实践去提高这个技能。希望这篇文章能让你对写代码这件事有更多的理解。
更新时间: 34个月前 版本: 7c48ad151
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
Google软件工程之工具篇
Google软件工程之文化篇
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
Google软件工程之文化篇
20 July 2022 25:51 | 5687 字 | 10266 次阅读
软件工程
软件工程 • Tech Lead • Pair Programming
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
编程与软件工程
软件工程发展史
Google软件工程文化
团队协作的基石
打造知识共享文化
领导团队(Tech Lead)
工程效率测量
总结
在软件工程的概念被提出之前,IT行业经历了软件危机。当时IT行业开发的软件正在经历从小规模到大规模的过程,而没有系统化的方法论指导大规模软件的开发过程,导致软件工程师之间的协作非常的低效且质量难以得到保证。
直到IT行业意识到软件需要一种工程化的方法论来指导开发过程。
软件工程是将系统化的、规范的、可度量的方法用于软件的开发、运行和维护的过程,即将工程化应用于软件开发中(IEEE,1993)。
编程与软件工程
编程是利用某个编程语言实现某个算法或技术方案从而来解决某类问题,但这类问题规模一般很小,也不太会随着时间产生变化,或者其生命周期也很短,不需要考虑长期维护的问题。但软件工程与编程的区别在于前者需考虑时间与规模带来变化的影响。
时间的因素使软件工程需要考虑质量的问题,糟糕的质量会产生很多认知负荷,最终难以长期维护。规模的因素使软件工程需要考虑协作的问题,低效的协作可能与沟通方式有关,最终拖垮交付进度。
软件工程发展史
早期的软件工程借鉴了项目管理的流程,而传统的项目管理关注生产过程大过于人。
质量管理
人力资源管理
沟通管理
干系人管理
风险管理
时间管理
成本管理
范围管理
维护
部署
集成
测试
开发
设计
需求
软件项目管理
软件开发过程
传统软件工程
以过程为中心的软件工程过程方法论主要有瀑布式与统一软件开发过程。这种软件开发过程需要产生大量的正式文档,通过严格的流程管理控制软件的开发过程。但软件开发过程却是一个知识密集型的生产过程,过于关注流程而忽略人的因素导致这类方法论很难适应需求变化,花费很多时间开发出的产品很难满足变化迅速的市场,最终逐渐被淘汰。
以人为中心的软件开发过程是为适应需求变化的代码编写和团队组织方法论。这类逐渐产生以极限编程与敏捷过程两种方法论。
敏捷(Agile)
极限编程(XP)
统一软件开发过程(RUP)
瀑布式(Waterfall)
以人为中心
以过程为中心
软件开发过程方法论
这里以敏捷过程为例,敏捷开发至今已经成为很主流的软件开发过程。
Story Desk Check
Story Kickoff
Story Point
User Story
工作坊(Workshop)
知识分享(Session)
结对编程(Pair Programming)
代码评审(Code Diff)
持续集成交付(CI/CD)
测试驱动开发(TDD)
Story
Backlog
Sprint
反馈(Feedback)
回顾(Retrospective)
成果展示(Showcase)
迭代计划会议(IPM)
每日站会
质量
进度
沟通
Kanban
Scrum
响应变化高于遵循计划
客户合作高于合同谈判
工作的软件高于详尽的文档
个体和互动高于流程和工具
实践
框架
价值观
敏捷过程
敏捷开发倡导信任人而非死板的流程与计划管理,通过一系列如上所示的实践来营造一个信任为主的文化土壤,从而打造一个高效的研发团队,最终开发出高质量的软件成品。
当然本文不是介绍敏捷开发过程的,而是介绍Google公司的软件工程实践。
Google是一家伟大的互联网公司,同时研发出来了大量知名的软件产品。Google是怎么做到的?在2020年Google的三位工程师@TitusWinters @manshreck @hyrumwright出版了《Software Engineering at Google》这本书来介绍Google的软件工程实践,这给了我们一个机会一窥究竟。
本文是Google软件工程系列的上篇之文化篇,来介绍软件工程最重要的一个基石:文化要素。
Google软件工程文化
以下是《Software Engineering at Google》一书第二部分文化篇的思维导图,由于此部分占全书近20%,所以本文不会详细的介绍其中的概念,想详细了解的读者建议阅读原书。本文会结合此书这部分内容分享作者的个人理解及相关经验。
评审
注释
项目干系人
团队成员
代码
技术文档
技术演讲
内部社区
邮件列表
群组
定性指标(Qualitative)
定量指标(Quantitative)
满意度(Satisfaction)
节奏和速度(Tempo and velocity)
认知复杂度(Intellectual complexity)
工程师注意力(Attention from engineers)
代码质量(Quality of the code)
保护好精力
学会放弃
解决重要的委托,委托紧急的问题
通过成功的循环模式解决难题
划分问题空间
建立自驱团队
决策,然后迭代
权衡利弊
识别盲点
在可控范围内给团队成员提供试错的机会
让团队成员知道当他们做的好时
保护团队成员免受无关因素干扰
在问题发酵膨胀前解决掉它
培养可替换自己的第二梯队成员
委托任务但也与团队成员一块解决问题
与团队成员沟通职业规划
一对一与成员沟通
匿名收集反馈
心情曲线
正面提出负面反馈
友善富有同理心的提出反馈
量体裁衣式指导团队成员
给团队成员成长机会
委托对的人解决问题
非技术问题
技术问题
构建团队共识
通过提问帮助成员解决问题
始终保持冷静
当犯错时及时道歉
接受团队成员反馈
信任团队成员
谦虚不自大
非职权
职权(Authority)
简报(Newsletter)
代码库(Codebase)
社区(Community)
知识库(Wiki)
以教促学
问好问题
从遗留知识中学习
群组交流模式
导师制度
指标(Metrics)
信号(Signals)
目标(Goals)
谁在何时决定对结果采取行动?
如果结果与预期相反,会采取什么行动?
如果数据符合预期,会采取什么行动?
期望的结果是什么?为什么?
总是在扩大规模
总是不在场
总是在做决策
其它建议
追踪团队成员幸福感
真诚
给团队设定清晰目标
成为团队老师和导师
扫清团队绊脚石
做团队催化剂
做一个禅师
不自我
把团队成员当孩子看待
打破招聘门槛
做老好人
忽视人性问题
忽视表现差的团队成员
招聘听话的团队成员
架构
开发
领导力
团队知识沉淀
个人学习成长
营造心理安全的环境
知识禁区
鹦鹉行为
团队知识断层
知识单点故障
知识孤岛
缺乏心理安全
GSM框架
是否值得测量
大规模团队领导力
正面模式
反面模式
核心能力
塑造团队学习文化
专业知识共享的挑战
信任(Trust)
尊重(Respect)
谦虚(Humility)
工程效率测量
领导团队(Tech Lead)
打造知识共享文化
团队协作的基石
工具
过程
文化
Google软件工程
文化为何是软件工程很重要的一个要素?我在刚入IT行业时对软件开发的理解只是学习某个编程语言来写代码。之后经过多年多个项目的历练,我逐渐对整个软件开发的全貌有了一定的理解。但还是停留在对软件开发过程的了解及相关工具的使用,直到读完这本书的文化篇后才意识到了文化的重要性。
文化虽然是和技术没有直接关系的一些软性的东西,但文化就像水一样,利用好它的力量,可以让软件开发变得更高效、高质。
团队协作的基石
软件开发早已不是单兵作战的时代了,虽然人们喜欢听黑客奇迹般的故事,但现在的中大型软件都是团队的集体智慧产出。
如上是两大顶级开源基金会 @TheASF 与 @linuxfoundation 的知名项目,成千上万的软件工程师的集体协作开发了达上亿的代码行数最终才诞生了这些伟大的项目。
而团队协作最重要的是成员间的谦虚、尊重与信任。如果没有这些最基本的条件,集体协作将会困难重重,团队成员无法形成稳固的合作关系,时间和精力很容易被内耗完。
打造知识共享文化
软件工程与传统工程的区别在于其是一项知识密集型的活动,其中涉及到了大量的知识管理工作。知识管理很容易做差,最终产生了以下的一些问题:
缺乏心理安全:当一个新成员进入组织的时候,会因为缺乏心理安全感而不敢暴露自己不懂的知识,这会阻碍该成员的个人成长,最终也会传导到团队的开发效率上来。
知识孤岛:知识没有分享的文化土壤,每个团队都闷头搞自己的项目,会逐渐产生很多重复造轮子的现象。
知识单点故障:团队内的重要信息只有一个人知道,而这个人又没有分享给其他人,一旦这个人不在场,团队将无法正常运转。
团队知识断层:团队内的技术专家并没有意愿将个人的经验传授给团队其它成员,导致团队的运转离不开一小部分技术专家,一旦这些人离开,整个团队将无法正常运转。
鹦鹉行为:这种很容易出现在团队新成员,由于经验限制或背景上下文了解有限,而在不了解其原理的情况下复制一些代码。虽然系统可以运行,但潜在的埋下了一些问题。
知识禁区:团队成员由于经验限制或不了解背景上下文,而不敢修改代码库的某些遗留代码。
这些都是组织学习文化中会遇到的一些挑战。Google在打造知识共享文化上做了很多尝试,比如营造安全的学习环境,鼓励新人通过提问与分享来提高个人的技术。这些实践在敏捷过程中也可以看到,比如:
鼓励个人反馈的文化(Feedback)。对事不对人,根据事实对成员提供建设性的反馈,帮助成员变的更好。
鼓励团队成员定期做分享(Session)。比如每周某个成员可以做某个主题的Session,这种实践能让分享者对分享的主题有更深的了解。
每日站会同步工作进度。不仅可以让其它成员了解你所工作的范围,还能告知其它成员潜在的交付风险。
定期回顾(Retro)。团队定期举行迭代回顾的会议,回顾最近工作中好的、不好的的方面及相关建议,最终制定出一些行动来提升团队的工作效率、质量与幸福感。
团队Wiki工作区。团队知识沉淀的体现,新人可以通过查阅这些资料迅速了解项目背景上下文。
代码评审(Code Diff)。代码评审在Google的软件工程中是一项全员必须执行的实践。代码评审可以提高团队成员的编码水平,发掘代码潜在Bug,识别代码坏味道,形成统一的编码风格,共享业务与技术知识,消减团队知识断层的影响,甚至可以解决鹦鹉行为与知识禁区的问题。比如我所在的项目上团队所有开发成员每天会拿出一小时做集体的Code Diff,虽然成本不低,但其收益也很高。
结对编程(Pair Programming)。结对编程看起来是两个人在做一个人的活,但一般也有两种模式:乒乓模式(Ping-Pong)与驾驶员观察者模式(Driver-Observer),前者适合以TDD的方式开发,后者适合老带新。结对编程是成本高但非常高效的知识共享的实践,需结合项目实际的带宽决定当前是否采用此模式开发。比如我们项目会在交付时间不紧张时采用此开发模式。
以上的一些实践需结合组织或项目的实际带宽来决定是否使用,比如Code Diff一般是建议或者强制的,而结对编程是可选的,分享(Session)和回顾可能是定期或不定期的。
领导团队(Tech Lead)
当做了一段时间独立贡献者(Individual Contributor)后,可能有机会做Tech Lead了。Tech Lead是一个团队必备的角色,具有一定的职权影响力,当然更多的是采用非职权影响力去带领团队。
如何领导团队是个复杂的工作,对技术人员来说,从技术到管理是很难的事情。
项目干系人
团队成员
保护好精力
学会放弃
解决重要的委托,委托紧急的问题
通过成功的循环模式解决难题
划分问题空间
建立自驱团队
决策,然后迭代
权衡利弊
识别盲点
在可控范围内给团队成员提供试错的机会
让团队成员知道当他们做的好时
保护团队成员免受无关因素干扰
在问题发酵膨胀前解决掉它
培养可替换自己的第二梯队成员
委托任务但也与团队成员一块解决问题
与团队成员沟通职业规划
一对一与成员沟通
匿名收集反馈
心情曲线
正面提出负面反馈
友善富有同理心的提出反馈
量体裁衣式指导团队成员
给团队成员成长机会
委托对的人解决问题
非技术问题
技术问题
构建团队共识
通过提问帮助成员解决问题
始终保持冷静
当犯错时及时道歉
接受团队成员反馈
信任团队成员
谦虚不自大
非职权
职权(Authority)
总是在扩大规模
总是不在场
总是在做决策
其它建议
追踪团队成员幸福感
真诚
给团队设定清晰目标
成为团队老师和导师
扫清团队绊脚石
做团队催化剂
做一个禅师
不自我
把团队成员当孩子看待
打破招聘门槛
做老好人
忽视人性问题
忽视表现差的团队成员
招聘听话的团队成员
架构
开发
领导力
大规模团队领导力
正面模式
反面模式
核心能力
领导团队(Tech Lead)
对于此我的个人经验也不是很丰富,所以想了解此部分的推荐阅读此书原文。当然下面这些文章也不错:
The Definition of a Tech Lead
What Does a Software Tech Lead Do?
Tech Lead
如何应对团队协作的五大障碍
技术决策与团队认知负载
工程效率测量
没有测量就没有优化。只有测量到团队的工程效率后,我们才有可能制定提升效率的行动。Google设计出了GSM框架来测量工程效率。
定性指标(Qualitative)
定量指标(Quantitative)
满意度(Satisfaction)
节奏和速度(Tempo and velocity)
认知复杂度(Intellectual complexity)
工程师注意力(Attention from engineers)
代码质量(Quality of the code)
指标(Metrics)
信号(Signals)
目标(Goals)
GSM框架
工程效率的测量一般发生在大规模团队或组织级别,我所经历的项目上并没有此实践。对小型团队来说,可以通过简单的一些问卷调查这类定性的方式来收集团队成员的反馈,当然也可以通过一些量化的指标如流水线构建速度、迭代开发速率、代码静态分析结果、测试覆盖率等指标测量团队的工程效率。
推荐进一步阅读的文章:
敏捷交付的工程效能治理
总结
软件工程是一项复杂的知识工程,这让其区别于传统的项目管理。Google的软件工程文化与以人为中心的敏捷过程所倡导的理念有很多相似之处。
但反观国内很多软件公司虽然也用上了敏捷过程的方法论,但底层的文化土壤还是以过程为中心的方法论,对团队成员的不信任,没有分享文化,团队领导一言堂还是存在的。希望这种现象能随着国内IT行业的逐渐成熟越来越少吧。
更新时间: 25个月前 版本: 0600dd487
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
AI驱动开发:从Prompt到Product(直播)
我与ChatGPT结对编程的体验
Google软件工程之工具篇
Google软件工程之过程篇
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
从技术难题中学习
24 June 2022 16:35 | 3650 字 | 5503 次阅读
个人成长
如何学习 • 技术方法论 • 自我提升
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
难题的成因
设置目标
确定问题边界
分析难题
找出矛盾
寻求帮助
从文档中找答案
从源码中找答案
提问的智慧
从周围人寻求帮助
从互联网中寻求帮助
记录并分享
从解决难题中成长
之前写过一篇如何学习一门技术的文章,介绍了我在学习新技术的一些经验。不过这种按计划的学习方式效率可能并不高,真正高效的学习方式是在解决问题中实践所学的知识。作为工程技术人员,在项目中遇到问题的概率是高的,这给了我们机会去不断提升自己的能力边界。
某天项目上生产系统突然出现了一个性能问题,监控不断的产生某个API响应时间超出阈值的告警。经过短暂的排查,发现是我们这个API在处理时请求的另外一个服务下线导致的响应时间大幅增长。这个API在处理时会发起多个异步的请求去获取需要的资源,看起来是这个HTTP客户端在异步请求无响应时会让整个API响应的时间大幅度增加。
难题的成因
一般的问题可能需要我们半天的时间就能得到解决,但难题的出场总让人一脸困惑。为什么会这样?一般有以下的原因:
有限的时间;
复杂的环境;
知识的盲区;
大多难题是伴随上述三个原因而出现的。如果时间是无限的,我自然可以慢慢去学习研究,消灭知识盲区,把复杂的环境搞清楚,问题自然也会得到解决;如果环境很简单,定位问题就很容易,那么问题也就不难;如果我对问题的解集不存在知识盲区,我当然知道问题的产生的技术原因,自然也能解决掉它。
而解决难题的方法就诞生于对这三个原因的逐个攻破。
初步分析后,我们发现这个问题发生的原因很奇怪。因为对HTTP客户端做超时时间的限制了,也就是所有的异步请求都不应该超出这个时间限制,但实际上API的响应时间增长了很多。这个问题的复杂之处在于:整个API在跑在一个Nginx Worker进程上的JVM的一个线程里,这个线程又会调用一个线程池做I/O任务的处理,整个代码又是Clojure语言开发的。因为我们的服务是公共服务,这个问题是因集群中某个服务下线触发的一个性能问题,而服务的下线又是一个很可能发生的事情,所以时间也有限制。整个难题的出现完全满足环境复杂、知识盲区与时间限制。
设置目标
难题之所以难,是因为需在对此知识有盲区的前提下,在有限的时间内解决。在对难题解决的第一步是确定时间的限制,如果问题很紧急,那我们需要先找到一个方法把紧急的时间限制转变为一个较宽松的。比如可以先把线上出问题的版本回滚,或采取更容易驾驭的技术方案,或制定新的发布计划等。
在将难题的时间限制放宽松后,难题依旧可能有复杂的环境和知识的盲区,而这两个要素会使问题难以解决,最终影响整个项目的正常交付开发。所以在这一步要给自己或团队设置一个阶段性的目标,比如花费一定的时间将难题所处复杂的环境转变为在简单的环境可复现问题,如果在这个时间段依旧解决不了问题,就转而去寻求外部的帮助。这样可以将问题的影响限制到预期范围内。
由于是线上出现的问题,我们的代码并没有做过修改,是因为所依赖的某个服务下线导致的突发问题。这时候只能先把这个下线的服务临时再次上线让线上环境先正常运行。这时候我们有了充足的时间来解决这个问题了。
确定问题边界
确定难题的边界是解决复杂环境要素的第一步。因为难题的根因和表象距离可能很远,从表象去分析问题很可能会浪费时间。这时候需要通过一些方法去找到问题的边界。
由于是线上出现的问题,第一步要做的就是在线下或非生产环境重现这个问题。幸运的是这个问题在线下得到了重现,如果不能重现那就是一个更难的问题了。但无法解释的还是为什么HTTP客户端异步请求的超时时间没有生效。难道是最终阻塞等待的函数有问题?也可能是底层多线程池的问题?或者是这个库在某种特定的场景下会出现这个Bug?
分析难题
对难题初步的分析可能会引发多个方向的猜测,而这些猜测很多都是错误的,如果一个个试错,成本太高。这时候需要利用已有的经验去分析问题,无论是个人或团队都可以通过头脑风暴去分析难题,寻找矛盾。在这个过程中要大胆假设,小心求证,也需要对习以为常的经验抱有怀疑态度,因为一不小心就掉入知识盲区的陷阱里了。
在经过一段时间的讨论分析之后,我们尝试过一些方法。比如我们测试发现HTTP客户端在同步请求模式下超时时间设置是正确的,无论请求的Socket链接是否能正常建立,都可以在规定的超时时间生效,那就可以在外部通过多线程的方式用这个HTTP客户端的同步请求模式。因为Clojure的core.async库支持类似Go语言的CSP并发模型,于是我们测试了这种方案,甚至上线了一个版本。最终又在线上发现这个版本会导致底层Nginx的Worker进程异常退出,看起来陷入了另一个知识盲区里了。经过搜索后发现core.async库使用了协程的方式,但它并不会像Go语言的运行时一样将阻塞的I/O请求使用I/O多路复用来处理,所以无法在它的Go任务块里执行阻塞I/O请求。最终经历了一圈尝试,发现问题依旧没有得到解决。
找出矛盾
找出问题的矛盾点是解决知识盲区这个要素有力的武器。任何问题都有矛盾点,这个矛盾点就是问题的根因。如何找出矛盾?在复杂环境中寻找矛盾会受到很多干扰因素的影响,所以需要通过简化环境的办法寻找矛盾。比如通过在本地搭建一个最简的环境来模拟问题产生的环境,也可以将复杂的功能或组件通过删除或注销的方式停止。
既然core.async的方案行不通,甚至陷入了新的知识盲区,那就是我们对业务系统所使用的Nginx-Clojure框架并不了解,看起来要解决这个问题,不仅要考虑业务代码使用的库,还需要考虑其所运行的Nginx环境。在查阅了其官方文档后,发现Nginx-Clojure已经考虑到这个问题了,甚至提供了多种解决方案,我们选择了其中的协程的方案。并在本地环境配置好了,但更奇怪的问题出现了,按照官方的指导配置,实际压测的结果却非常的差。这个结果一度让我们很迷茫,看着这个star不多的库,想到在配置的过程中遇到很多按文档操作不成功的例子,只能通过issue和源码的方式去找可用的方式。难道只能推倒重来,重写解决问题?
为了进一步排除可能是知识盲区导致的问题,我们又用Go语言重写了一个Goroutine的版本做压测的基准对比,也与业务系统的其他API做对比,结果显示这个协程版本的压测结果都差到难以接受。
看来只能寻求外部帮助了。
寻求帮助
以上都是用已有的经验去诊断未知的问题,如果运气不好或问题无法被上面的方法所锁定,那我们只能去寻求外部帮助了。外部帮助是解决知识盲区有效的办法,前提是能找的正确的知识或人。
从文档中找答案
最基本的知识盲区是我们不了解所用的技术,解决这类盲区可以通过查阅官方文档,最起码要了解所用技术最基本的用法,这样就算找人问也可以清楚的描述问题。如果文档极其的多,那可以利用一定的搜索技巧,比如尝试关联的关键词去搜索相关的文档。
从源码中找答案
当文档不准确或者过于简单,只能通过阅读相关的源码来了解一些底层的实现。阅读源码是一种很好的提高自己技术能力的方式,这也是难题给我们创造了一种可能不得不读源码来解决问题的场景。从这个角度看,难题是能提高我们对底层框架实现的了解。如何更好的读代码,可以阅读这篇文章:
如何阅读一份代码?
我是怎么读代码的
如何阅读一份源代码?(2020年版)
为了确定是否是HTTP客户端的影响,我走读了clj-http的源码,发现它在异步请求模式下底层封装了HttpAsyncClients。于是写了一个Java版本做对比,发现存在相同的问题,很可能是因为异步模式下需要做很多重量级的初始化工作导致的问题。于是给提了一个issue,但没有得到回复。
提问的智慧
当文档和源码都无法告诉我们答案时,唯一的办法是去咨询更厉害的人。当然找人帮助,需要一些好的提问技巧。
从周围人寻求帮助
当周围人有相关的经验时,最简单是找这些人帮忙。但在找之前,准备好必要的上下文(如果是公司项目,需考虑脱敏),必要时可以画一些流程图或技术图,这样可以更高效的获取有效的帮助。
从互联网中寻求帮助
在互联网上寻求帮助,要想免费得到答案,需要一些提问的智慧。至少也需要在问题中详细的描述所做的尝试及相关的环境、测试等信息,这些都能帮助其他人快速的了解到我们的问题。
最终我在做了很多测试后,不得不给Nginx-Clojure提了一个issue。这个issue也得到了作者的回复及最终解决问题的办法。这个难题得到了最终的答案。
记录并分享
在解决难题的过程中,我们学习了很多未知的知识,甚至做了很多技术尝试。这些过程如果被记录下来,那会是不错的笔记。如果能写成文章,不仅能帮助他人,也能收获影响力。这种学习方式被称之为Learn In Public。
从解决难题中成长
解决难题是一种非常高效的主动学习方式。难题是一面镜子,能反映我们的知识盲区。在解决难题的过程中,不仅可以解决知识盲区,还能提升解决问题的能力,甚至能得到更厉害的人的帮助,也有可能收获影响力。
因解决某个“难题”而给K8S容器镜像构建工具Kaniko提的PR
因解决某个“难题”而给K8S容器镜像构建工具Kaniko提的PR
更新时间: 35个月前 版本: 8171f612c
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
如何学习一门技术
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
编程语言是如何实现并发的之并发模型篇
27 May 2022 26:39 | 5864 字 | 12452 次阅读
编程语言
编程语言 • 并发编程 • 线程安全
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
常见的并发模型
多进程(Multiprocessing)
多线程(Multithreaded)
共享内存通信(Shared memory communication)
Lock
STM
消息传递通信(Message passing communication)
CSP
Actor
Vert.x
事件驱动(Event Driven)
Event Loop with Multiplexing
编程语言的实现
Java
Go
Erlang/Elixir
Clojure
走向高并发
总结
参考资料
在操作系统篇中介绍了从操作系统的视角中不同编程语言实现并发的共同的一些概念。本文将会介绍常见的并发模型及不同编程语言是如何实现的。
常见的并发模型
Vert.x
Scala/Akka
Erlang/OTP
Go
Clojure
Java/Go/Clojure/C
Netty(Java)
Twisted(Python)
Redis
Node.js
Nginx
Actor
CSP
STM
Lock
Event Loop
with Multiplexing
消息传递通信
共享内存通信
Fork
事件驱动(Event Driven)
多线程(Multithreaded)
多进程(Multiprocessing)
并发模型
多进程(Multiprocessing)
多进程模型是利用操作系统的进程模型来实现并发的。典型的是Apache Web Server,每个用户请求接入的时候都会创建一个进程,这样应用就可以同时支持多个用户。
如上图,在Linux系统中PID为1的名为P0的进程通过fork()系统调用创建了3个子进程。在多处理器的系统中,这些子进程可以并行运行在多个处理器上,当然这些进程也可以被同一个处理器通过上下文切换进行CPU分时共享。
在图中M1、M2与M3都代表内存资源,在多进程中如果不同进程想共享内存中的数据必须通过进程间通信的方式来实现。
多进程模型的缺点在于创建进程的开销非常高,如果进程过多会很快消耗光系统资源,并且上下文切换在进程间的开销也很高,在I/O密集型任务的应用中,此并发模型很难满足需求。
多线程(Multithreaded)
在操作系统的视角看,比如Linux中,在进程中创建线程是通过clone()系统调用来实现,这和创建子进程的区别不大。线程与进程的区别在于同一个进程内的线程共享着进程分配的资源,线程不被分配资源,只是操作系统调度执行任务的抽象的最小单元。
比如下图中,PID为10的进程P0通过clone()系统调用创建了3个线程,这些线程都可以访问进程分配的内存资源M0。
我们也可以通过htop命令在Linux中看到这些进程及线程的运行信息,比如下图中所示PID为339328进程(它是一个基于JVM的Clojure应用)及其创建的线程信息:
也可以通过/proc文件系统获取进程创建的线程的信息:
$ ls /proc/339328/task/
1043773 339340 339342 339344 339346 339348 339350 339357 339359 339361 339363 339367 339380 339388 788478 975443 991563
339328 339341 339343 339345 339347 339349 339353 339358 339360 339362 339364 339378 339382 424498 975439 991561
相比多进程模型来说,因为线程比进程创建的系统开销小,所以多线程模型是很常见的实现并发的方式。但此种模型存在一个必须解决的问题,就是线程间通信的问题。但线程为什么要通信呢?那是因为大部分业务系统问题的解空间在用冯·诺伊曼计算机去实现的时候,都存在并发计算时线程间数据共享的问题。要数据共享有两种方式:
共享内存通信(Shared memory communication):不同线程间可以访问同一内存地址空间,并可修改此地址空间的数据。
消息传递通信(Message passing communication):不同线程间只能通过收发消息的形式去通信,数据只能被拥有它的线程修改。
共享内存通信(Shared memory communication)
因为线程间共享内存资源,在访问临界区域时会出现数据竞争(发生竞态条件,即代码的行为取决于各操作的时序)的问题,如果不能正确的处理此问题,程序会产生线程不安全的问题,最终导致程序崩溃或无法正常运行。
解决竞态条件的方式是对数据进行同步(Synchronize)访问。要实现同步访问常见的方式有:
锁(Lock):通过锁定临界区域来实现同步访问。
信号量(Semaphores):可以通过信号量的增减控制对一个或多个线程对临界区域的访问。
同步屏障(Barriers):通过设置屏障控制不同线程执行周期实现同步访问。
Lock
锁(Lock),也叫互斥量(Mutex)。线程在操作临界区域资源时,需要先获取锁,然后才能操作,当操作完成后,需要释放锁。此模型利用了对底层硬件运行过程的形式化,这让其即简单又复杂。从锁的种类就可以看出来其复杂性:
自旋锁
递归锁
乐观/悲观锁
公平/非公平锁
独享/共享锁
偏向/轻量级/重量锁
分段锁
对锁的使用不当还会产生死锁问题(Deadlock)。在实际开发过程中,能不用锁就不用锁,可以考虑使用一些轻量级的替代方案如原子变量(Atomic),或无锁(lock-free)非阻塞(non-blocking)算法实现的数据结构。
原子变量的更新为何是线程安全的?因为CPU提供了CAS(Compare-and-swap)的指令来更新原子变量,这条指令从硬件上确保了此操作是线程安全的。
此模型的优点:
大多编程语言都支持此模型;
贴近硬件架构,使用得当性能很高;
是其他并发模型的基础;
此模型的缺点:
不支持分布式内存模型,只解决了进程内的并发同步;
不好调试与测试,想用好不容易;
STM
锁模型是一种悲观的并发同步机制,但实际上冲突发生的概率并不高,所以乐观的并发同步机制性能更好。STM(Software transactional memory)就是这样一种用来代替锁模型的乐观并发同步机制。STM是用软件的方式去实现事务内存(Transactional memory),而事务内存中的事务(Transactional)正是关系型数据库中的概念,一个事务必须满足ACID性质,如下图所示:
在t0时刻,T1、T2与T3线程同时获取要操作的同一数据的快照v0,之后T1线程在自己的事务里于t1时刻提交自己的写入值v1,之后T2线程在自己的事务里提交自己的写入值v2,由于在提交时刻会做冲突检测,此事务发现操作数据的快照已经发生变化,于是回滚自己的提交。之后开启新的事务重新获取最新的快照v1,并于时刻t2成功提交自己的写入值v2。在线程v3中由于是读取操作,并没有数据修改,所以在它的事务中使用的是最早的快照v0。
在STM的事务中尽可能避免副作用,比如在事务中去修改原子变量这种操作,可能会导致事务回滚失败。
STM实现的一种方式是基于MVCC(Multiversion concurrency control)。很多编程语言提供了这种并发模型的实现。
此模型的优点:
相比锁模型更简单;
大部分情况下更高效;
此模型的缺点:
在事务内需要避免产生副作用;
不支持分布式内存模型,只解决了进程内的并发同步;
消息传递通信(Message passing communication)
在锁模型中,生产者和消费者之间的通信是通过共享内存而完成的,要实现安全通信,必须给共享内存所属的临界区加锁。那如果生产者和消费者是通过消息传递完成通信的呢?那样我们就可以摆脱锁模型的限制了。
Do not communicate by sharing memory. Instead, share memory by communicating. Communication forces coordination. (Ivo Balbaert)
CSP
通信顺序进程(CSP(Communicating sequential processes))是一种形式语言,用来描述基于消息传递通信的安全并发模型。如下图所示:
这些任务块之间的通信是基于通道(Channel)来完成的,当创建了一个通道之后,不同的任务块就可以通过持有这个通道来通信,通道可以被不同的任务块共享。通道两端任务块的通信可以是同步的,也可以是异步的。
在这里的任务块不是如Java里重量级的线程类,在运行时是非常轻量级的代码块。这些代码块可以被调度到不同的线程中,最终被多个CPU内核并发执行。
此模型的优点:
相比锁模型更简单;
很容易实现高并发;
此模型的缺点:
不支持分布式内存模型,只解决了进程内的并发同步;
Actor
演员模型(Actor)是一种类似面向对象编程思想的安全并发模型。在面向对象的世界里,对象是一种封装了状态及行为的实体,对象间通过消息去通信(通过对象调用其方法)。而在Actor模型中,一切皆Actor,每个Actor中都有自己的状态,其他Actor只能通过通信的方式来获取或修改被通信Actor的状态。Actor通信的方式类似收发邮件,它有自己的收件箱,如下图所示:
在上述图中,我们可以看到相比CSP模型,Actor模型可以跨节点在分布式集群中运行。实际上Actor模型的代表Erlang正是天然分布式容错的编程语言。
此模型的优点:
相比锁模型更简单;
很容易实现高并发;
支持分布式内存模型,能实现跨节点的并发同步;
此模型的缺点:
存在信箱满后消息丢失的问题;
Vert.x
Vert.x是一个基于JVM的反应式模型的工具包,在解决并发同步的问题上它采用了类似Actor模型的方式,如下图所示:
在它的架构里最基本的计算单元名为Verticle,这些Verticle之间通过事件总线(Event Bus)进行异步通信,当然也可以像Actor一样跨节点通信。和Actor模型不同的地方在于Vert.x支持多种语言,因为Event Bus解耦了实现Verticle的语言限制。
事件驱动(Event Driven)
在多线程方式实现的并发模型中,我们解决问题的方式是通过创建更多的线程来提高系统的并发处理能力。但线程创建的开销及线程间上下文调度切换的开销并不是很小,所以纵使系统的硬件资源很充足,也存在一定的上限。那么有没有可能只创建一个线程,而且这个线程可以同时处理很多个任务呢?当然是可以的,这正是基于I/O多路复用的事件循环处理并发模型的解法,通过单线程来并发处理I/O密集型的任务。
Less is more.
Event Loop with Multiplexing
此模型巧妙的利用了系统内核提供的I/O多路复用系统调用,将多个socket连接转换成一个事件队列(event queue),只需要单个线程即可循环处理这个事件队列。当然这个线程是有可能被阻塞或长期占用的,针对这种类型的任务处理可以单独使用一个线程池去做,这样就不会阻塞Event Loop的线程了。
此模型的优点:
单线程对系统资源的占用很小;
很容易实现高并发;
此模型的缺点:
不支持分布式内存模型,只解决了进程内的并发同步;
编程语言的实现
许多编程语言标准库或三方库都已支持上述大多数的并发模型,但因为一些历史原因带来的兼容性问题,开发者的使用体验好坏不一。以下仅简单介绍下各种编程语言标准库对并发模型的实现及流行三方库的扩展支持。
Java
Java是一门面向对象的编程语言,标准库对并发的支持是基于共享内存通信的锁模型,因此用Java的标准库来实现高并发是一件非常有挑战的事情,想不踩坑太难。
想深入了解Java的并发模型,可以参考《Java并发编程实战》。
当然基于Java的三方库实现了很多并发模型,如:
Actor: Akka
Event Loop with Multiplexing
Netty
Nginx-Clojure
Go
在Go流行的时期流传着一个故事:一个PHP的普通开发者在一周内学会了Go语言,之后开发出了一个高并发的Web应用,要用Java实现同样的性能,至少需要多年的经验。
暂且不论这个故事是否合理,但它展示了Go语言的两大亮点:
语法简单易学;
天然支持高并发模型;
Go在语言层面实现了CSP并发模型,因此能让开发者以非常低的成本写出高并发的Web应用。在对CSP并发模型的实现中,Go任务块一般是一个函数,这个函数的调度是由Go语言的调度器来完成,可以被调度在不同的线程中。如果在这个函数中出现了阻塞线程的如网络I/O的操作,调度器会委托给Netpoller去执行,而Netpoller的底层正是对操作系统I/O多路复用技术的封装。
Go高并发的秘诀在于它的G-P-M运行时调度模型,详细的设计可参考这篇文章:Scheduling In Go : Part II - Go Scheduler。
Erlang/Elixir
Erlang是一门天然分布式、高并发、容错的编程语言,它是Actor并发模型的代表编程语言。Elixir是基于Erlang虚拟机(BEAM)的一种不纯粹的、动态类型的函数式语言。它们自然原生支持Actor并发模型,所以在开发高并发的分布式容错应用时,可以考虑使用Elixir,它强大的并发模型及富有表达力的语法可以提供非常好的开发体验。
Erlang的虚拟机在运行时实现了软实时抢占式调度,详细的信息可参考这篇文章:How Erlang does scheduling。
Clojure
Clojure是基于JVM平台的Lisp方言,是不纯粹的、动态类型的函数式语言(这点倒和Elixir类似)。Clojure可以直接调用Java的库,这让其可支持非常多的并发模型,但最有特色的就是它的标准库实现了STM的并发模型,官方提供的异步库core.async也实现了CSP的并发模型。当然还可以通过Nginx-Clojure实现基于I/O多路复用的高并发模型。
Clojure支持非常多的并发原语,想了解可参考这篇文章:Clojure Concurrency Tutorial。
走向高并发
1999年的时候C10K的问题被提出,当时在单机上并发处理上万的连接是一件富有挑战的事情,最终借助操作系统提供的一些支持如进程调度与I/O模型,通过一些高并发的模型如I/O多路复用,我们可以在单机上支持上万甚至更多的并发连接。
二十多年过去了,互联网从Web1走向了Web3,联网的设备从PC走向了IoT,我们已经到了万物互联的时代。C10K的问题已经转变为C10M的问题,如何在单机上支持百万乃至千万的连接呢?
在这篇The Road to 2 Million Websocket Connections in Phoenix中我们可以看到如何用Elixir的Web框架Phoenix在一台40核128GB内存的机器上支撑两百万的Websocket长链接,最终因Linux内核的限制而无法进一步提高并发链接数,并没有达到服务器的极限。
Migratorydata甚至使用Java在一台12核96GB内存的机器上支撑了上千万的Websocket长连接,更多细节见这两篇文章:
How MigratoryData solved the C10M problem: 10 Million Concurrent Connections on a Single Commodity Server
Reliable Messaging to Millions of Users with MigratoryData
当单机并发达10K时,内核是解决方案,当单机并发达10M时,内核是瓶颈。比如Linux内核需要32GB内存来维护上千万的Socket连接。所以在单机千万级的解决方案需要在内核之外的应用层去做更多的事情,感兴趣的读者可以看这篇文章:The Secret to 10 Million Concurrent Connections -The Kernel is the Problem, Not the Solution。
总结
在软件开发过程中,安全至关重要。编程的两大安全难题是线程安全与内存安全。这个系列的两篇文章都是在尝试介绍不同编程语言是如何解决线程安全从而更容易的实现高并发。
另外一个想借助这篇文章分享的一个观点是:问题可以被更复杂的方案去解决,当然也可以通过另一种截然不同的思路去更简单的解决。当你觉得一个方案很复杂的时候,试着去找一个完全不同方向的方案,也许会更容易一些。
参考资料
I/O Models
Concurrency is not Parallelism (it’s better)
Callback vs Promises vs Async Await
Sync vs. Async vs. Concurrent vs. Parallel
What are Linux Processes, Threads, Light Weight Processes, and Process State
Latency Numbers Every Programmer Should Know
Cooperative User-Level Threads (Coroutines)
Green threads - Wikipedia
multithreading
Ask HN: Erlang and network connections?
Thread (computing) - Wikipedia
Concurrent Programming for Scalable Web Architectures
Concurrency Models
Software Transactional Memory
Vert.x concurrency model
The event loop - JavaScript | MDN
Faster async functions and promises · V8
透过rust探索系统的本原:并发篇
并发模型
七周七并发模型
更新日志
2022-05-27:初稿发布。
2022-05-07:开始写作。
更新时间: 25个月前 版本: c3bbbb7ab
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
编程语言是如何实现并发的之操作系统篇
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
编程语言是如何实现并发的之操作系统篇
10 April 2022 25:35 | 5629 字 | 13070 次阅读
编程语言
编程语言 • 并发编程 • 线程安全
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
从操作系统运行程序说起
操作系统的支持
调度(Scheduling)
线程(Thread)
用户线程(User-level Thread)
线程模型(Thread Model)
上下文切换(Context switching)
I/O模型(I/O Model)
并发还是并行
总结
从操作系统运行程序说起
这是一台拥有2个虚拟CPU核心的Linux服务器的系统监控界面。其中红框①中PPID代表父进程ID,PID代表进程或线程ID。红框②中CPU代表当前线程运行的CPU核心编号。红框③中是程序的运行命令,其中绿色代表的是线程,白色为进程。
以PID为1375的进程为例,它的父进程为1086,可以通过PPID不断追溯至PID为1的init进程。从这可以看出Linux通过fork()的系统调用不断的复制出大量的需要被执行的程序进程。
进程是操作系统进行资源分配的一个独立单位,而实际在CPU运行调度的是线程。以进程1375为例,它又创建了7个线程,如下:
\( ls /proc/1375/task/
1375 1429 1430 1431 1432 1433 1488
\)
ls /proc/1375/task/1429
arch_status cgroup cmdline cpuset exe gid_map loginuid mountinfo ns oom_score patch_state root sessionid smaps_rollup statm uid_map
attr children comm cwd fd io maps mounts numa_maps oom_score_adj personality sched setgroups stack status wchan
auxv clear_refs cpu_resctrl_groups environ fdinfo limits mem net oom_adj pagemap projid_map schedstat smaps stat syscall
其中线程1430与1432是调度运行在2号CPU核心上的,如果持续观察这个监控界面,会发现同一个线程会不定时在两个CPU核心之间来回切换,这实际正是操作系统对这些线程在多核CPU上进行抢占式调度。
操作系统之所以能用有限的CPU核心去运行非常多的程序,并且用户感觉这些程序是在同时运行。一方面操作系统(内核)可以通过一些方法实现并发处理任务(程序),另外一方面得益于多个CPU核心,操作系统还可以并行处理任务。
本文并不是研究操作系统是怎么实现并发的,但在搞清楚编程语言是怎么实现并发处理之前,很有必要提前对操作系统支持并发提供的一些重要特性做一个全面的介绍。操作系统为了支持多任务处理,提供了进程管理与调度,同时在I/O上提供了多种访问文件或网络的系统调用方式。
操作系统的支持
Verticle
Green Thread(Java)
Erlang process
Goroutine
Coroutine
信号驱动(Signal Driven)
多路复用(Multiplexing)
非阻塞式(Non-blocking)
阻塞式(Blocking)
纤程(Fiber)
内核线程(Kernel-level Thread)
用户线程(User-level Thread)
协作式(Cooperative)
抢占式(Preemptive)
异步(Asynchronous)
同步(Synchronous)
执行方式
调度方式
I/O
进程(Process)
操作系统
调度(Scheduling)
进程调度主要有抢占式调度和协作式调度两种:抢占式(Preemptive)与协作式(Cooperative)。
抢占式与协作式任务调度(Preemptive Multitasking vs. Cooperative Multitasking)
抢占式与协作式任务调度(Preemptive Multitasking vs. Cooperative Multitasking)
抢占式调度往往在一些重要位置(Sleep Call,Timer Tick)放置了中断信号,通过这个信号通知操作系统调度器(Scheduler)进行进程切换。在抢占式模型中,正在运行的进程可能会被强行挂起,这是由于这些中断信号引发的。
协作式调度也叫非抢占式调度,是指当前运行的进程通过自身代码逻辑出让CPU控制权。与抢占式调度的区别在于进程运行不会被中断信号打断,除非其主动出让控制权给其他进程。
以上描述并没有明确区分进程还是线程,实际在Linux系统的内核态上,用户态的线程统一按轻量级进程(Light-weight process)来处理,它们与真正的进程的区别是在一个用户态进程中的线程共享了相同的地址空间和其他资源(如打开的文件描述符),但在内核调度上并没有什么区别。
线程是进程的运行实例,哪怕在非多线程的进程中,在内核态实际运行进程的还是内核线程,所以接下来介绍下线程的分类。
线程(Thread)
我们在编程语言中见到的线程,一般指的是与内核线程一一映射的用户态线程,比如Java中的Thread其实就是内核线程。但一些编程语言如Erlang与Go都实现了更轻量级的用户线程。
用户线程(User-level Thread)
协程(Coroutines - Cooperative User-Level Threads):应用程序通过线程库自行实现的协作式调度的用户线程,代表性的有Go语言的Goroutine(1.14之前的版本),Vert.x框架中的Verticle。
Go Goroutine:Go语言的Goroutine在1.14之前是协程的机制,之后的版本采用了异步抢占式调度(asynchronously preemptible)。
Erlang process:Erlang VM(BEAM)管理的用户线程,与协程相比的优势在于它可以做到公平调度,不会出现协程中某个用户线程占用过多CPU周期。
绿色线程(Green Thread):类似于协程,是由Java JDK实现的,但因为早期Linux系统没实现内核态的抢占式调度,Green Thread只能在Solaris系统上发挥它的威力,最终在JDK 1.3之后被Native Thread取代。所以现行的JDK的线程实际是非常重量级的内核态线程,Java的Project Loom会尝试实现新的Green Thread方案,并且是抢占式的调度方案。
内核线程(Kernel-level Thread):操作系统内核管理的抢占式调度的线程,是最终运行在CPU上实际执行任务的最小单元。
纤程(Fibers):操作系统内核管理的协作式调度的线程。这一系统级别的方案最终因硬件和软件的发展逐渐式微,现在的用户线程也能达到协作式调度的效果。
用户线程与内核线程的区别在于用户线程的调度是发生在用户态,内核中无法感知到用户线程的存在,并且调度也发生在用户态,一般是由线程库或编程语言运行时自行实现的。而内核线程的调度是由内核完成的,一般是抢占式调度。
用户线程(User-level Thread)
用户线程大多是采用协作调度的方式实现,本质上是同步执行在与CPU核心数量相同的内核线程上的,不仅能极大的降低了上下文开销,还能最佳的利用多核CPU的计算能力。
用户线程的轻量除了体现在调度的上下文切换开销上,还体现了在对内存的需求上。如果要在4核心4GB内存的笔记本电脑中测试同时生成100万个线程的话,不同编程语言对内存的需求:
Elixir: 0.48 GB (Process)
Golang: 1.91 GB (Goroutine)
Java: 977 GB (Thread)
PHP: 6836 GB (Laravel Request)
数据来源:Programming ElixirChapter 15
但用户线程的执行最终是由内核线程来完成,所以存在一个从用户线程到内核线程的映射模型。
线程模型(Thread Model)
可能有人会疑惑,用户线程与内核线程是一一映射的吗?总的来说有以下三种线程模型:
不同的线程模型(Different Threading Models)
不同的线程模型(Different Threading Models)
1x1 (kernel-level threading):用户线程与内核线程是一一映射的。这是最简单的模型。在这种语境下,用户线程中的线程就是我们常规意义上说的线程,当程序创建一个线程时,也会在内核中创建一个内核线程。目前大多数操作系统如Linux、Solaris、FreeBSD、macOS与iOS内核的线程模型就是这种。
Nx1 (user-level threading):多个用户线程与一个内核线程映射。在这种模型中,内核线程只有一个,在应用内部不存在内核线程切换的开销,程序的并发能力是很高的。但一旦某个用户线程被阻塞(发生网络或文件I/O系统调用),其他用户线程也会被阻塞。另外应用也无法从多核CPU上获得更好的并发性。所以实际的使用场景中,这种模型并不常见。
MxN (hybrid threading):多个用户线程与多个内核线程映射。这种模型最为复杂,但也是最强大的线程模型,不仅有着很好的并发能力,同时还能获得多核CPU的处理能力。早期的Solaris内核中也支持这种线程模型,但因为其会导致内核调度过于复杂,逐渐被放弃。但在Erlang VM和Go语言的运行时就又实现了这种线程模型。
早期Solaris内核的线程和轻量级进程(Threads and Lightweight Processes)
早期Solaris内核的线程和轻量级进程(Threads and Lightweight Processes)
轻量级进程(Light-weight process):Solaris内核中的概念,但也会在其他系统内核中看到类似的概念。指的是用户线程和内核线程之间的接口,也可被认为是一个调度用户线程执行的虚拟CPU。当用户线程发出系统调用时,运行该线程的LWP调用内核并保持绑定到该内核线程至少直到系统调用完成。当LWP在内核中为用户线程执行系统调用时,它会运行一个内核线程。因此,每个LWP都与一个内核线程相关联。只有在用户线程完全由轻量级进程构成时,才可以说轻量级进程就是线程。Go语言中Goroutine的G-P-M调度模型中,P承担了类似LWP的角色。
应用程序为什么会费劲的设计出用户线程?操作系统提供的内核线程不香吗?这是因为操作系统内核的线程切换是重量级的操作,它需要进行上下文切换,而这会很耗时。
上下文切换(Context switching)
在进程间切换需要消耗一定的CPU时钟周期进行相关的状态管理工作,包括寄存器和内存映射的保存与读取、更新各种内部的表等。比如在Linux内核中,上下文切换需要涉及寄存器、栈指针、程序计数器的切换。
在这篇How long does it take to make a context switch?中可以看到一个结论是:
Context switching is expensive. My rule of thumb is that it’ll cost you about 30µs of CPU overhead…Applications that create too many threads that are constantly fighting for CPU time (such as Apache’s HTTPd or many Java applications) can waste considerable amounts of CPU cycles just to switch back and forth between different threads…I think the sweet spot for optimal CPU use is to have the same number of worker threads as there are hardware threads, and write code in an asynchronous / non-blocking fashion.
因为线程调度的上下文切换成本非常昂贵,所以最佳的做法是应用程序使用和CPU核心相同的线程数,这样每个线程都能充分利用CPU核心的时间片,避免了应用内部多个线程的上下文切换开销。
用户线程的轻量级让应用程序能够极大的提高并发性,但也会有一些问题,比如某个用户线程中发起一个网络请求导致底层的内核线程被阻塞,因为多个用户线程在共用这个内核线程,最终导致大量的用户线程被阻塞。
解决这个问题需要了解操作系统的I/O模型。
I/O模型(I/O Model)
当应用程序需要访问文件或者网络资源时,应用内的线程将会花费大量的时间来等待数据的到来。如下图所示:
如果把CPU处理计算机指令的速度类比成高铁的速度,那线程一次文件或网络的访问将会和蜗牛一样慢,这相当于你在高铁上让蜗牛去帮你取快递,对CPU来说是巨大的浪费。现代操作系统已经提供了多种I/O模型来解决这个问题。常见的I/O模型有:
五种I/O模型对比(Comparison of the five I/O models)
五种I/O模型对比(Comparison of the five I/O models)
同步(Synchronous)
阻塞式(Blocking):同步阻塞式是最常见的I/O模型。线程在访问文件或网络资源时,会因发起了内核的系统调用被挂起,内核会检查文件描述符是否可读,当文件描述符中存在数据时,内核会将数据复制给线程并交回控制权,线程从挂起状态切换成可运行状态等到内核调度运行。
非阻塞式(Non-blocking):线程在访问文件或网络资源时,因文件描述符是非阻塞的,线程在检查数据是否可读的阶段是非阻塞的。此模型需要线程不停的通过轮询(polling)的方式检查文件描述符是否可读。但之所以属于同步I/O,是因为在最终读取数据(recvfrom)时需要从内核态中拷贝数据(recvfrom)到用户态中,这个阶段线程依旧被阻塞住无法处理其他指令。
多路复用(Multiplexing):和非阻塞式的区别在于,多路复用模型的线程可以同时访问多个文件描述符,这很适合构建高并发的Web服务器或中间件。但此模型会在检查文件描述符时会被阻塞(select),并且在读取数据(recvfrom)时也会被阻塞。和多路复用模型相似的是使用多线程和阻塞I/O,但当线程产生很多时会消耗大量的内存资源以及线程调度产生的上下文切换开销,所以多路复用模型一般只使用单线程模型。
信号驱动(Signal Driven):和上面的模型区别在于,之前的模型都需要线程主动轮询,信号驱动模型需要监听内核的SIGIO事件,通过注册事件处理函数,之后线程可以继续执行其他任务。当数据可读时,线程处理函数会以阻塞的方式从内核态复制数据到用户态。
异步(Asynchronous):此模型和同步模型最大的区别在于,不仅在获取文件操作符时不会被阻塞,数据从内核态复制到用户态也不会被阻塞,因为内核会去做这个复制数据的工作,线程只需要在回调函数中使用数据即可。
关于I/O模型的更多细节,请参考:
Study Notes - I/O Models
I/O Models
思考一个问题:如果用户线程不可避免的需要被挂起进(如访问共享资源但没有获得锁)而导致内核线程被挂起,如何调度可以让此内核线程上的用户线程可以继续运行?
并发还是并行
前面讨论了很多和I/O相关的概念,并不是所有的计算机任务都是和I/O相关(I/O bound)的,比如很多算法都需要CPU做大量的计算,这时候CPU核心根本不会因为等待外部资源而空转,如果在这种计算密集型(CPU bound)任务中使用多线程技术,那么就会产生大量的线程上下文切换开销,最终会导致处理任务的性能变慢。
在这篇Scheduling In Go : Part III - Concurrency文章中,作者对比了两种类型工作在并发和并行模式的对比,并提供了一些经验总结:
CPU密集型(CPU bound):如果任务是CPU密集型的,那么使用并行的方式来解决问题,这样可以最大的利用CPU核心的算力。如果使用并发的方式去处理,反而会增加复杂度。
I/O密集型(I/O bound):如果任务时I/O密集型的,那么使用并发的方式来解决问题,这样可以提升单个CPU核心的吞吐量(Throughput)。
并发(Concurrent)与并行(Parallel):并发是指同时处理(dealing with)很多事情,并行是指同时做(doing)很多事情。并行是并发的特殊情况,只能在计算机多核环境中实现。
同步(Sync)与异步(Async):一种编程模型。区别在于同步是可预测(predictable)的,异步是不可预测(unpredictable)的编程模型。
总结
本篇从操作系统的视角介绍编程语言实现并发的底层概念,包括进程调度与I/O模型等。下篇开始介绍常见的并发模型。
更新日志
2022-04-15:根据此讨论帖修改更新。(感谢@lxdlam)
2022-04-10:初稿发布。
更新时间: 31个月前 版本: edcbaaf71
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
编程语言是如何实现并发的之并发模型篇
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
编程语言是如何实现泛型的
25 March 2022 30:18 | 6666 字 | 10582 次阅读
编程语言
编程语言 • 泛型 • 参数多态 • 模版 • 参数化类型
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
从Go引入泛型说起
什么是泛型
类型系统(Type system)
类型内存布局(Type Memory Layout)
类型检查
非泛型突破类型的限制
手工复制(Manual code duplication)
代码生成(Code generation)
类型断言(Type assertation)
反射(Reflection)
接口(Interface)
实现泛型
类型擦除(Type erasure)
虚函数表(Vtable)
字典(Dictionary)
单态化(Monomorphization)
模版(Template)
蜡印(Stenciling)
总结
参考资料
从Go引入泛型说起
本月,Go 1.18版本发布,一个大的变化是引入了正式的泛型支持。此时距离Go的第一次泛型提案已经过去了快十二年了!而花费如此长的时间引入的特性却引发了很多讨论与质疑。如在Reddit和Hacker News上这两个热门的帖子:
1.18 is released : golang
Go 1.18 | Hacker News
Go为什么要引入泛型?又为什么会花费如此长的时间?带着这个疑问让我们来看看编程语言中泛型的魔力吧。
什么是泛型
先看维基百科中对泛型编程(Generic programming)的定义:
Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters. This approach permits writing common functions or types that differ only in the set of types on which they operate when used, thus reducing duplication.
如果上面这个非常学术化的定义让你无法理解的话,让我们先从一个简单的例子来说明泛型的作用。如下是个简单的C语言函数:
int compare(int a, int b) {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
这个函数的存在的问题是不灵活,如果调用方需要比较两个浮点数,那还需要编写一个浮点数的函数,这样就导致了很多仅有类型差异但实现重复的代码。
如果你写过JavaScript这种动态的编程语言就会发现根本不需要类型的约束。如下是JavaScript版本的算法:
function compare(a, b) {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
那么问题来了,为什么如C这种编程语言需要显式的类型约束呢?
类型系统(Type system)
类型内存布局(Type Memory Layout)
当应用的数据在内存中存储时,操作系统看到的只是一串比特序列,数据类型就是给这串比特序列一定的意义。比如有显式类型含义的编程语言中如C中的int8就会把比如11111111这串比特序列看作十进制的127,而uint8就会把这串比特序列看作十进制的255。显然同一串内存比特序列的含义因类型而异。这样就可以理解为什么C语言中的compare函数只能处理int而无法处理long了,因为编译成机器指令的代码只能处理int类型的数据。
在JavaScript中Number的内存大小是8个字节,而String的内存大小是16个字节,但只是一个指针引用,具体的内容大小是不确定的。
但JavaScript的函数为何可以处理多种类型的数据呢?这是因为JavaScript是一个弱类型的动态类型检查编程语言,而C却是一个弱类型的静态类型检查编程语言。
类型检查
强弱类型:强弱类型并没有标准的定义,但是普遍认为强弱类型的核心区别在于,语言能否在某一个时刻能检查出来类型导出的错误,而不是抛出运行时错误(Unchecked RuntimeError)。强类型的编程语言可以在类型不匹配时发生错误(编译与运行时都可能发生),而弱类型的语言在类型不匹配时会做隐式的类型转换或无视类型进行操作,这会导致无法预料的运行时错误。这二者区分出来的核心现象就是,弱类型语言往往无法信赖变量的值,需要写很多额外的代码做额外的类型验证操作。
静动检查:动态类型一时爽,重构代码火葬场。开发过大型项目的人一定会对这句话深有体会。动态与静态的区别在于类型检查发生的阶段,动态是在运行时阶段,静态是在编译阶段。但实际上一些编程语言是混合的类型检查,比如在C中开发者可以通过关键字来标识此数据类型检查是动态还是静态的。不少静态类型检查的编程语言也有动态的类型检查,比如Java中既有编译阶段的静态类型检查,又有运行时的动态类型检查(如父类与子类的互相转换)。
类型推导:一些编程语言虽然不需要开发者显示定义数据类型,但编译器能够做类型推导,帮助开发者定义数据类型,如Scala与Kotlin。
现在再回到起初的那个问题上,我们可以看到C与JavaScript同为弱类型的编程语言,真正的区别在于动态类型检查上。这让JavaScript的函数可以不受类型检查的限制,而C却受到了编译阶段的类型检查。
以Go语言为例,类型检查发生在编译阶段,如下:
词法分析
语法分析
类型检查
中间代码生成
代码优化
机器码生成
思考一个问题:对一个强类型的编程语言来说,类型检查发生在编译阶段还是运行时分别有哪些差异?
非泛型突破类型的限制
由于类型检查对静态类型检查编程语言的限制,导致我们在实现一些算法时,由于这些算法对不同数据类型有着相同的处理流程,如果我们要实现一套针对所有类型都有效的算法库,就需要编写大量的重复代码。
在编程语言没有泛型特性的黑暗年代,存在这么一些古老的方法来解决对泛型的需求。
手工复制(Manual code duplication)
任何语言都可以通过这种最简单的方式手动实现泛型的效果,除了对程序员很不友好之外,这种方法有以下一些优点:
产生的重复代码相比自动生成的代码要少。
相比运行时的方法性能要高。因直接调用函数开销比通过运行时做额外的类型检查要低。
代码生成(Code generation)
Java的ASM与Go的genny都可以做到自动代码生成,其他语言也都可以实现类似的功能。这种方法解放了程序员,虽然有以下的一些问题:
需要一些集成的手段去使用这些库,可能让代码构建变的更复杂。
增加了编译时间。
增加了二进制包的体积。
类型断言(Type assertation)
这种方法通过将函数中的参数类型转换为根类型(如Java中的Object或Go中的interface{}),然后对根类型进行期望的类型断言。实现可参考Go的类型断言示例代码。
这种方法有以下一些问题:
调用方需要将参数包装或转换成根类型。
实现方代码中耦合了大量的类型断言代码。
失去了编译器的类型安全保障。
反射(Reflection)
与类型断言类似,不过是用反射的技术在运行时获取类型信息,通过对类型的枚举判断来实现。实现可参考Go的反射示例代码。
这种方法有以下一些问题:
可读性可能不太好,因为用到了复杂的反射技术。
会导致运行时性能差。运行时反射要比直接的代码多了很多指令操作,所以性能要慢很多。
失去了编译器的类型安全保障。
接口(Interface)
SOLID设计模式中的依赖倒置原则(Dependency Inversion Principle)要求软件接口在设计中应该依赖抽象而不是具体。比如在Java中方法参数可以设计成一个interface,之后可以针对此接口进行不同的实现,这样的设计也更容易写出可测试的代码。
根据这个指导原则,我们也可以用此实现泛型。
Comparable
+len()
+less()
+elem()
ComparableInt
-[]int values
+len()
+less()
+elem()
ComparableFloat
-[]float values
+len()
+less()
+elem()
这样在我们的泛型方法里只需要依赖Comparable接口,调用方可以传入具体的类型如ComparableInt或ComparableFloat。
这种方法有以下一些问题:
可能需要定义很多数据类型。
实现泛型
通常意义下的泛型也叫参数多态,指的是声明与定义函数、复合类型、变量时不指定其具体的类型,而把这部分类型作为参数使用,使得该定义对各种具体类型都适用。参数化多态使得语言更具表达力,同时保持了完全的静态类型安全。这被称为泛化函数、泛化数据类型、泛型变量,形成了泛型编程的基础。
编程语言理论(PLT)中多态(Polymorphism)包含三个主要方面:特设多态(Ad-hoc),参数多态(Parametric)和子类型(Subtyping)。
Ad-hoc:也叫重载(Overloading),允许具有相同名称的函数对不同类型执行不同的操作。例如,+运算符即可以将两个整数相加,也可以连接两个字符串。
Subtyping:也叫包容性多态(Inclusion),是指通过基类指针和引用使用派生类的能力。
子类型多态(Subtyping)是面向对象编程(OOP)中很重要的一个概念,它也称为运行时多态性,因为编译器在编译时不定位函数的地址,而是在运行时动态调用函数。这也称为动态派发(Dynamic Dispatch)。
派发目的是让程序运行时知道被执行的函数或方法所在的内存位置。派发分为:
静态派发(Static dispatch/early binding):当程序在编译时可以找到执行的函数。C++默认使用的是直接派发,加上virtual修饰符可以改成虚函数表(Vtable)派发。直接派发是最快的,原因是调用指令少,还可通过编译器进行内联等方式的优化。这种派发缺点是不灵活,无法实现一些面向对象所需的技术如多态性。
动态派发(dynamic dispatch/run-time dispatch/virtual method call/late binding):当程序在运行时可以找到执行的函数。Java默认使用的是虚函数表(Vtable)派发,通过final修饰符可改成直接派发。虚函数表派发是有动态性的,一个类里会用表来存储类成员函数的指针,子类重写(Override)父类的函数会替代父类的函数,子类添加的函数会被加到这个表里。当程序运行时派发时会从这个表中找到对应的函数,这样就可以实现动态派发。面向对象的编程语言正是靠此机制实现了多态性(Polymorphic)。
消息机制(message passing):通过消息传递来调用被执行的函数。这种机制是在运行时可以改变函数的行为,甚至函数可以未实现,也不会引发运行时错误。比如Objective-C中就是通过消息传递来调用被执行的函数,甚至可以在程序运行过程中实现热更新代码。
以上三种派发方式都有其优劣:比如静态派发的速度是最快的,但并不灵活。而动态派发虽然比较慢,但却可以实现面向对象多态的功能。消息机制是最灵活的方式,但性能也最差。
前面我们已经看了手工实现泛型的多种方式。为了解放程序员,各静态类型检查的编程语言都实现了泛型,走的路线却各不相同。如下图:
GC Shape Stenciling Go
蜡印(Stenciling)
模版(Template) C++
Witness table Swift
Go
单态化(Monomorphization)
字典(Dictionary)
类型擦除(Type erasure) Java
实现泛型的方式
类型擦除(Type erasure)
对Java来说统一的数据类型就是Object,在编译阶段做完类型检查后就将类型信息通过转换成Object进行擦除,这样只需要生成一份泛型函数的副本即可。类型擦除保证了泛型函数生成的字节码和非泛型函数的是相同的,也符合Java对兼容性的要求。不过类型擦除也给Java的泛型带来了很多的限制。
虚函数表(Vtable)
之前在动态派发中介绍了虚函数表(虚方法表),对Java来说通过类型擦除结合虚方法表来实现泛型的效果:运行时同样的数据类型Object,却能调用原始类型的方法。
VTable0
+equals()
+hashCode()
+getClass()
+toString()
«Data Layout»
Object
vptr
other Object data members
VTable1
+equals()
+hashCode()
+getClass()
+toString()
+length()
+charAt()
«Data Layout»
String
vptr
other Object data members
other String data members
字典(Dictionary)
编译器在编译泛型函数时只生成了一份函数副本,通过新增一个字典参数来供调用方传递类型参数(Type Parameters),这种实现方式称为字典传递(Dictionary passing)。
Go实现泛型的方式,就是在编译阶段,通过将类型信息以字典的方式传递给泛型函数。当然这个字典不仅包含了类型信息,还包含了此类型的内存操作函数如make/len/new等。
同样的Swift也通过字典传递了一种名为Witness table的数据结构,这种数据结构包含着类型的大小及类型的内存操作函数(移动、复制与释放)。如下图:
«Vaule witeness Table for trival type, size 4, align 4»
ValueWitnessTable
size: 4
alignment: 4
copy: memcpy
move: memcpy
destroy: no-op
«Metadata»
Int
value witness table
reflection information
«Vaule witeness Table for reference type»
ReferWitnessTable
size: 8
alignment: 8
copy: retain
move: trivial
destroy: release
«Metadata»
MyClass
value witness table
reflection information
Swift在编译阶段通过在函数入参中以字典的形式把Witness table注入,达到了以统一的方式处理任何类型,而不用对类型进行装箱操作。这样一来,Swift就可以实现泛型,而且不需要单态化,虽然在运行时有动态查找的开销,但这种表结构节省了分配内存和缓存不一致性的成本。
func getSecond
return pair.second
}
//上述函数编译后变成下面的函数,入参的T就是Witness table的数据结构
void getSecond(opaque *result, opaque *pair, type *T) {
type *PairOfT = get_generic_metadata(&Pair_pattern, T);
const opaque *second = (pair + PairOfT->fields[1]);
T->vwt->copy_init(result, second, T);
PairOfT->vwt->destroy(pair, PairOfT);
}
单态化(Monomorphization)
单态化的思路是自动生成多个类型的泛型函数版本,看起来就是一个模版代码生成的过程,但是也需要考虑很多种情况。比如:
生成所有类型的函数版本:这种最简单,但是会拖慢编译时间,也会让最终的二进制文件变得很庞大。
生成调用类型的函数版本:这种需要编译器分阶段或多次编译,比如需要遍历寻找调用点来确定最终的类型列表,对于不同包的同名函数的处理等。
是否支持独立编译:如果调用泛型函数的类型与泛型函数不在同一个包内,是否能支持泛型函数独立的编译。
模版(Template)
C++通过模板实现泛型类、方法和函数,这导致编译器为每个唯一的类型参数集编译了代码的单独副本。这种方法的一个关键优势是没有运行时性能开销,尽管它以增加二进制文件大小和编译时间为代价。
蜡印(Stenciling)
蜡印其实就是模版,也是一种代码生成技术。但Go除了使用字典传递实现装箱外,还采用了GC Shape Stenciling的技术。这种看起来很高级的名词简单来说是为了解决蜡印或模版的问题,因为在蜡印的过程中,编译器会为每一个实例化的类型参数生成一套独立的代码,这种会有一个问题,请看下面的例子:
type a int
type b int
虽然a和b是两个自定义的类型,但它们底层都是int类型,但编译器会为这两个类型而生成两套函数的版本(如对max泛型函数来说会生成max_a与max_b两个函数版本)。这是一种浪费,还会生成庞大的二进制文件。GC Shape这种技术就是通过对类型的底层内存布局(从内存分配器或垃圾回收器的视角)分组,对拥有相同的类型内存布局的类型参数进行蜡印,这样就可以避免生成大量重复的代码。
Go实现泛型的方式混合了字典与蜡印技术,最终的流程如下:
开始编译
寻找泛型函数调用
使用GCShape对类型分组
为类型生成实例化的函数
创建包含类型信息的字典
调用实例化的函数并传递入参与类型字典
思考题:为什么Swift采用了和Go类似的字典装箱技术,却并没有混合使用蜡印技术?
总的来说Go在1.18实现泛型的方式和Rust挺类似的,如果感兴趣可以看这篇Rust泛型的文章:
透过Rust探索系统的本原:泛型
总结
对静态类型检查的编程语言,实现泛型的方式有很多,如:
C++:通过模版实现,相比C的宏,模版显然更强大灵活。
Java:通过类型擦除的装箱技术结合虚方法表实现,虽然类型擦除导致Java的泛型实现不如人意,但这种代价确保了兼容性。
Swift:通过字典传入的方式配合Witness table的实现,这种巧妙的方式在编译速度与运行时速度之间取得了不错的平衡。
Go:通过字典传入的方式配合GC Shape的蜡印技术实现,这种方式在编译速度与运行时速度之间取得了不错的平衡。
虽然看起来方式多样,但实际上只有两种思路:
Type Erasure/Dictionary:调用方法统一,运行时对代码进行检查。在这种场景下,要么方法表会被传入对应的泛型参数,要么在调用时编译器去解析(Resolve)。本质上这两种技术都用来实现某种RTTI的替代品。
Monomorphization/Stenciling:根据不同类型生成不同的代码。现代编译器已经支持了只针对使用到的类型进行代码生成了,C++的模板跟Rust的静态Trait都是此类。
总的来说泛型是个和编译技术强相关的概念,由于本文涉及了多种编程语言,文中可能存在一些因笔者能力有限而导致的错误,欢迎读者指正。同时一些编程语言如Go也在不断的改进自己实现泛型的机制,所以此文也会存在一些时效性的问题。
我希望能通过这篇文章帮助读者更好的理解泛型,尤其是多种编程语言的实现机制,以及语言设计者在泛型设计上的一些权衡利弊的考虑,这样也能有助于我们了解自己日常使用的编程语言,写出更好的代码。
参考资料
Models of Generics and Metaprogramming: Go, Rust, Swift, D and More - Tristan Hume
Comparing the implementation of generics | Thomas Denney
2017 LLVM Developers’ Meeting: “Implementing Swift Generics ”
proposal/generics-implementation-dictionaries-go1.18.md at master · golang/proposal
Go 1.18 type parameters | Let’s Go generics
Golang Generics Implementation Details - njkhanh
Dynamic vs. Static Dispatch | Lukas Atkinson
Go语言设计与实现(@Draven)
Go泛型是怎么实现的?
Swift -派发机制
类型vs.类型检查
更新日志
2022-04-24:根据此讨论帖修改更新。(感谢@FrankHB、@lxdlam)
2022-03-25:初稿发布。
更新时间: 38个月前 版本: ab9f763e8
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
编程语言是如何实现并发的之并发模型篇
编程语言是如何实现并发的之操作系统篇
我的个人项目技术栈
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
走进Web3
互联网诞生之初,大家期望能打造一个去中心化的(decentralized)分布式网络(distributed network),所以有了超文本协议(HTTP)、点对点传输协议(P2P,基于此技术的BT下载是互联网第一个去中心化的应用程序)。互联网的信息通过网页(HTML)展示,页面间通过超链接(hyperlink)串联起来。互联网的内容则通过P2P的方式共享分发。
之后随着PC与移动端的普及与大力发展,资本也涌入进来。互联网的数据与内容逐渐被少数中心化的(centralized)寡头公司掌握,我们的生活都被这些公司的产品所承包。个人隐私、数据逐渐被这些巨头收集分析,通过大数据与人工智能技术对用户画像,精准投放广告。在 在线广告 的加持下,互联网公司的市值也如滚雪球一般膨胀的停不下来。
作为用户我们讨厌互联网公司对隐私的收集、分析与贩卖,这种监视一切的感觉虽然讨厌但我们却离不开它们的产品。互联网公司的产品大多是免费的,这个秘诀就在于用户也是其产品的一部分。每个用户都在给平台的产品附加价值,由于网络效应,互联网产品的价值与用户数量成指数级的关系。典型的产品如IM类的微信,社交媒体的微博,用的人越多,越多人就离不开它。
正如「分久必合,合久必分」,互联网发展史上两股潮流也在你方唱罢我登场,这两股潮流就是「中心化」与「去中心化」。互联网从早期经历了Web1的去中心化然后走向了Web2的中心化,最终来到了Web3的去中心化。而这一切都是因为十多年前那个神秘人点燃了 火种。
一种基于 区块链 技术的名为比特币(Bitcoin)的应用被创造出来了,在 创世纪块 里甚至引用了泰晤士报当天的头版标题:财政大臣正站在第二轮救助银行业的边缘(Chancellor on brink of second bailout for banks)。这个火种如「星星之火,可以燎原」一样,烧出了「币圈」与「链圈」。
驱动比特币底层的去中心化的区块链技术启发了在加拿大的 Vitalik Buterin,之后他创立了以太坊(Ethereum)。以太坊的诞生产生了有计算能力的区块链(之前比特币是基于只能记账的区块链的应用),可编程(EVM)的链打开了Web3的大门。
互联网诞生之前,信息的流动非常的缓慢与低效,是 Web1 让信息流动起来。信息虽然流动起来,但只有少数人参与了进来。随着互联网的发展,参与的人越来越多,是 Web2 让海量的信息交互起来。但无论Web1还是Web2,都没有本质的区别,依旧是信息在参与流动与计算,但 Web3 让价值流动起来了。
Web3将催生一个全新的全球数字经济,创造新的商业模式和市场,打破像Google和Facebook这样的平台垄断,并产生大量自下而上的创新。(Gavin Wood)
要理解如何让价值流动起来,先要理解一些以太坊的知识。以太坊的EVM让链上具备计算能力,而天然去中心化的特性让其成为一个超级分布式的计算机。这个计算机有别与Web2中的云计算,云计算是中心化的超级计算机。在Web2中用户的数据(价值)不是掌握在用户手里的,而是掌握在一个个有着云计算的寡头公司手中,比如AWS、Google、Facebook、Twitter等。要让价值流动起来,就需要让价值从少数寡头公司中所属变为从属为网络中每个参与者。数字加密货币天然具备能解决这个问题的能力,当货币具备了计算的能力,自然也就有了价值流动的能力。
人类社会之所以能不断发展,是因为我们是一个协作的物种,大部分动物不会协作。而能协作是因为我们可以通过建立一个个合约与他人建立合作关系。在Web3之前,任何合约的正常履行无论是线下还是线上都依赖于国家这个机器的力量,我们都也知道违约的惩罚。
如何在不依赖外部力量去解决合约的正确履行呢?目前看到一个可行的方式是让合约代码化,赋予自动执行的力量。以太坊的计算能力体现在智能合约(Smart Contract)上,这正是以太坊的创新之处,区块链本身就具备记录价值的能力(分布式账薄),智能合约通过以太坊的EVM自动执行,确保了合约的正确履行。
智能合约的执行依赖于以太坊中的天然气(Gas),可以把它理解为现实世界中的电力一样,我们的一切生活所需电器都需要电力的支持,我们也需要定期缴纳电费。在以太坊的世界中,每个人都拥有一个钱包,钱包里存放着我们的数字资产如BTC与 ETH。
有了智能合约后,我们可以创造基于此的Web3应用,也被称为dapp(decentralized application) 的去中心化应用。dapp让普通用户接入了以太坊的世界。可以把dapp理解为Web2中的各类App,这些App提供了各类互联网的服务。
想象下从目前这些技术我们可以得到什么?打造Web3我们还缺什么重要的组成部分?「经济基础决定上层建筑」,打造Web3需要很多组织招募优秀的工程师加入,合力去打造新的生态。如何构建新形态下的组织?如何募集资金?传统的中心化组织形态(公司)和IPO(上市)已经不适合Web3了,要彻底的去中心化,需要创造新的经济形态。
让我们先回顾下Web2下的经济形态吧。在Web2中,互联网公司通过天使投资募集资金,组建工程师队伍,发布网站或App。通过互联网在线广告或会员付费等形式赚钱,达到一定规模后在交易所发行股票上市(IPO)。
Web3需要解决这些Web2中的问题:
组建公司
天使投资
打造Web3产品
IPO
在以太坊上只需要一定的Gas费即可凭空创造一个代币(Token)。这个代币可以代表任何东西,比如股权、实体资产、彩票、美元或虚拟游戏中的角色等。比如建立美元与加密货币价格桥梁的USDT就是一种代币(因价格锚定美元,又称稳定币)。如果你持有美元,想要获得比特币(BTC)或以太坊(ETH),除了 挖矿 外,可以直接通过在中心化交易所(如 币安)或与其他人(C2C)兑换。
有了代币以后,可以解决很多上述Web2中的问题。比如你洗澡的时候突然有了个天才的想法(灵感也可能来自Web2的产品),但你没钱招募人才。在Web3的世界可以直接发个代币(此时没啥价值),想要来钱快可以在交易所上币(ICO,方便割韭菜,但可能需要一大笔钱给交易所),一旦这种代币被别人接受(形成共识)后,就有了价值,作为发币方你就潜在的获得了大量的钱(浮动盈亏,要变现还得找个流动性好的市场)。
由于币是被凭空创造出来的,要让其有价值,就需要很多人形成共识(Consensus)。就像黄金一样,它的价值就在于几千年来代代相传的共识,让其有了坚不可摧的价值。想想钻石也一样,神奇之处就在于其稀缺性都没了,工业上可以大量创造出来,但由于其共识(韭菜)过于强大,导致市场价格很坚挺,这就是共识的力量。
要形成共识就需要让一部分人先有币(让一部分人先富起来),这部分有币的人为了自己的价值就会推广这个币。所以第一步就需要分发这些币,在币圈常见的做法是空投(Airdrop)。要让空投有价值,就需要对投币用户做一定筛选。比如我们的Web3产品是一款要颠覆Twitter的社交媒体产品,那最好的空投对象就是Twitter用户了。可以在智能合约中设定规则,按Twitter follower的数量来按比例投币。
现在你有了代币,甚至都不需要天使投资(也可以理解为市场中每个愿意交易你代币的人都是天使投资)就可以开始组建团队了,不需要组建公司(不然又回到Web2的世界了)。在Web3中公司就是DAO(Decentralized autonomous organization)。DAO的特点在于通过智能合约去制定组织章程,而非在Web2中公司章程这种。持有代币的人就可以成为DAO的一员,甚至可以对公司的决策产生影响。
在空投了50%的币给Twitter用户后,你给DAO预留了20%的币,给团队成员预留了20%的币,还剩10%的币预留。目前我们还没有产品呢,但已经解决了很多Web2需要提前解决的问题。😂
接下来就是打造Web3产品了。Web3产品的特点是啥?价值流动!如何让价值流动起来?通过钱包登录!把钱包类比成Web1的用户名/密码组合、Web2中的单点登录(SSO),通过钱包去标识用户。从比特币的创世纪块出现的时候,区块链就具备记录额外信息的能力,它不只是一个分布式账薄,也是一个分布式数据库,可以存储额外的信息。
由于钱包地址很长,所以有了 ENS,ENS就像Web2中的域名服务商。比如我的ENS地址是 madawei.eth。如果别人给我转账,不需要再输入一长串根本记不住的地址,只需要输入这个地址就可以了。ENS还可以给区块链中添加个人介绍的信息,比如博客地址、头像、社交媒体等,支付一定的Gas费就可以写入这些信息,其他人包括Web3应用都可以获取到这些共享信息。
另外一个有意思的Web3应用是 Mirror,Mirror类似于Web2中的Wordpress(世界上最流行的博客应用)。通过钱包(如 Metamask)登录后在Mirror上可以通过发布文章赚钱。但并不是Web2中通过广告赚钱,而是通过 NFT。
NFT是一种不可替代的代币,它使用区块链提供所有权证明,但不限制底层数字文件的共享或复制,其更像是一种在Web3世界里身份的证明。这两年 NFT正处于高速发展期,在游戏、音乐、电影等泛文化领域都有广泛的应用。
在Mirror里发布文章可以选择是否也发行NFT,在支付一定的Gas费用后就可以发布独一无二的NFT了。读者如果非常喜欢这篇文章,可以通过支付一定的ETH来收藏这篇文章。这个收藏并不是Web2里的收藏,而是在以太坊的区块链里写入了你的所有权证明。可能很多人难以理解为什么要对一篇文章付费呢?在Web2里你可以给作者打赏,但在Web3里通过NFT这种形式,读者获得了所有权证明,甚至可以进一步卖掉这个所有权证明,作者也获得了经济上的支持,有动力进一步创作了,对整个生态来说是良性循环。
郭宇的一篇文章在 Mirror 上的 NFT 收益
郭宇的一篇文章在Mirror上的NFT收益
Mirror的数据都存在在 Arweave 之上,Arweave的愿景是提供去中心化、可扩展和永久的链上数据存储。如果说以太坊是超级分布式计算机,那Arweave给这个计算机提供了一块无限大的分布式存储硬盘。
目前Web3还处于探索未来的各种可能性之中,所以涌现了很多新奇的东西,很多一瞬而过,少数成为了经典。目前最大的一个问题就是Gas费太贵了。
「在Web3呼吸都要钱」,说的就是Gas费。因为一旦需要写入信息到区块链就需要支付Gas费。当然以太坊也在着力解决此问题,比如目前的 LAYER 2 ROLLUPS 提案就是为了提高交易速度,降低Gas费。相信一旦解决了这个问题,又会有一次dapp的爆发出现。
当然另外一个目前很火的Web3应用领域就是去中心化金融(DeFi)。数字加密货币本身就起源于金融领域,把传统的金融业搬上Web3并做一定的创新是必不可少的一个环节。
Web3相比于Web2的主要吸引力之一,是用户不再是被出售的产品,而是因其积极贡献而被奖励的网络参与者。当然我也相信基于区块链的Web3可能只是未来的一个方向。也可能未来是Web2 + Web3这种模式,类似于目前的5G应用场景一样。Web3不是Web2的替代,更像是一个方面的提升。
但Web3会不会从去中心化逐渐发展为中心化呢?考虑到比特币的绝大部分算力目前都掌握在少数几个矿池公司手中,再精妙的算法也抵挡不了人性对利益的追求,用户为了方便连隐私都可以不要,更不会为了一个去中心化的技术问题而放弃中心化的便利,Web3这条去中心化之路注定漫长而难走。
Blockchain?
AI?
Other?
Web1
Web2
Web3
Web3
Web3
本文参考的文章有:
THE WEB3 REPORT
useWeb3
为什么去中心化很重要
NFTs和千粉理论
Arweave—不仅仅是存储这么简单
为什么Web3很重要
关于Web3:退休这两年我错过的技术趋势
Quarter I 2022
Web3.0
Dapp-Learning
Defining the web3 stack
My first impressions of web3
注:
由于Web3和钱相关,目前相比Web2存在不少安全问题。比如ENS中0宽字符可能会导致转账错误的问题,又比如 My first impressions of web3 这篇文章提到的NFT实验 中Web3基础设施的安全问题会导致用户资产丢失。
由于Web3还处于探索阶段,不少资料或者概念都有一定歧义或不准确的地方,如果本文有错误的地方,欢迎留言指正。
本文 Mirror 地址。
更新时间: 40个月前 版本: e6a50e55c
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
SQLite的文艺复兴
Real-time Web应用开发新体验
分布式系统中的时间
分布式系统下的认证与授权
加密币挖矿小记
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
分布式系统中的时间
24 December 2021 33:59 | 7477 字 | 12741 次阅读
分布式技术
物理时钟 • Lamport timestamp • 逻辑时钟 • 分布式系统 • 向量时钟 • 混合逻辑时钟
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
多出一秒让互联网暂停
现实世界中的时间
时间的两个特征
物理时钟
时间标准
时钟仪器
时钟同步
时间表示
时钟类型
分布式系统中的时间
重新认识程序
因果一致性
逻辑时钟
Lamport timestamp
Happened-before
算法描述
一个例子
局限性
Vector clock
局限性
冲突策略
应用场景
结语
参考文章&&进阶阅读
GPS时
协调世界时(UTC/Zulu time)
国际原子时(TAI)
全序(Total order)
偏序(Partially ordered set)
原子时
相对论力学时
历书时(Ephemeris Time)
太阳时
恒星时
实时协作应用(Collaborative)
CRDT
强制因果通信
数据冲突检测
分布式节点状态同步(Replication)
分布式锁
布隆时钟(Bloom clock)
混合逻辑时钟
Vector clock
Lamport timestamp
因果排序(Causal ordering)
序理论(Order theory)
狭义相对论(Special Relativity)
单调时钟(Monotonic Clock)
墙上时钟(Wall Clock)
Unix Time/Timestamp/Epoch time/Posix time
RFC 3339
ISO 8601
TrueTime
北斗授时
GPS
NTP
PTP
原子钟
石英钟
机械钟
太阳钟
与谐波振荡相关
与天体运动相关
与地球自转相关
应用场景
算法
相关理论
时钟类型
时间表示
时钟同步
时钟仪器
时间标准
逻辑时钟
物理时钟
时间
多出一秒让互联网暂停
2012年6月30日,很多互联网在线服务系统和网站突然同时宕机,包括知名的社区Reddit:
这并不是什么大规模黑客攻击或底层基础库如最近的Log4j安全事件引发的,而是一个被人忽略的和时间相关的现象导致的错误。
WIRED的这篇 The Inside Story of the Extra Second That Crashed the Web 文章详细的报道了这个事件,原因简单来说是:
由于地球自转速度并非恒定,导致每隔一段时间协调世界时(UTC)与太阳时(Mean Solar Time)、世界时(UT1)出现微小的偏差。国际地球自转服务(IERS)基于实际地球自转确定何时必须插入闰秒(leap second)来纠正协调世界时与地球自转的偏差,于是他们决定在2012年6月30日最后一秒插入闰秒。
由于闰秒的特殊性,很多IT应用系统选择直接忽略它,依靠操作系统来处理这多出一秒的影响。问题就出在Linux Kernel对闰秒的处理上产生了Bug。
Linux Kernel的一个子系统Hrtimer负责唤醒因等待CPU时间片而睡眠的进程。但因为闰秒的影响,Hrtimer在Linux内核还未完成任务前提前1秒唤醒了睡眠的进程,进而导致了CPU过载而无法正常工作。
由于Linux是很多互联网服务的操作系统,这导致了大量的互联网服务停止服务,整个互联网按下了暂停键!
很难想象因为月球、潮汐等对地球自转的影响,间接导致了整个互联网暂停🤯。但这就是无处不在时间的力量。
当然Linux内核最终修复了这个错误,对技术细节感兴趣的可以看这篇 Resolve Leap Second Issues in Red Hat Enterprise Linux。
更有意思的是同样的事情在五年后又发生在了知名CDN厂商Cloudflare上,这次闰秒事件的起因是Cloudflare的DNS服务是用Go语言开发的,而当时的Go并没有单调时钟(后面会介绍到)的API。闰秒配合Go没有单调时钟的缺陷这一组合拳直接打的Cloudflare猝不及防。具体分析见这篇文章 How and why the leap second affected Cloudflare DNS。
现实世界中的时间
时间的两个特征
从维基百科对时间的定义中提到:
借着时间,事件发生之先后可以按过去-现在-未来之序列得以确定(时间点/时刻),也可以衡量事件持续的期间以及事件之间和间隔长短(时间段)。
哲学家对于时间有两派不同的观点:一派认为时间是宇宙的基本结构,是一个会依序列方式出现的维度,像艾萨克·牛顿就对时间有这样的观点。包括戈特弗里德·莱布尼茨及伊曼努尔·康德在内的另一派认为时间不是任何一种已经存在的维度,也不是任何会“流动”的实存物,时间只是一种心智的概念,配合空间和数可以让人类对事件进行排序和比较。
这里面无论是定义,还是哲学家的观点,都提到了时间和事件发生序列的关系。
在物理学爱因斯坦的相对论中,时间与空间一起组成四维时空,构成宇宙的基本结构。时间与空间都不是绝对的,观察者在不同的相对速度或不同时空结构的测量点,所测量到时间的流逝速度是不同的。在狭义相对论中,发生在空间中不同位置的两个事件,它们的同时性并不具有绝对的意义,我们没办法肯定地说它们是否为同时发生。
在相对论中,时间的定义复杂了很多。因为参考系的不同,时间中的序列的先后次序也有了一些不同。在同一个参考系中,随着时间的推移,事件出现的先后次序有因果关系。但在不同参考系中,事件出现的先后次序并没有因果关系。也就是说视角不同,事件的相互次序很难被判定。
明白时间的两个特征对之后我们理解分布式系统中的时间问题有很大的帮助:
在同一个参考系(视角)中事件发生的先后次序是确定的,可以判定事件的因果关系;
在不同参考系(视角)中事件发生的先后次序是不确定的,不一定能判定事件的因果关系。
物理时钟
人类很早就尝试通过一些方法丈量时间了,比如早期的太阳钟(日晷):
时间标准
太阳钟和地球自转现象相关,和地球自转相关的时间标准有恒星时与太阳时,而在太阳时下又分为民用时(平太阳时)和世界时(UT1)。还记得我们刚开始的和闰秒相关的故事吗?我们通过闰秒来纠正 协调世界时(UTC)和太阳时的差异。协调世界时(UTC)是与谐波振荡相关的时间标准下的原子时类别。在原子时里还有国际原子时(TAI)与卫星导航系统(GPS)使用的原子时。
时间标准反应了人们观察时间的视角。虽然原子钟是以原子的能级跃迁为基础建立的时间基准,但它追踪的是太阳时,也就是从地球自转的视角观察时间的流逝。还有一类是与天体运动相关的时间标准,比如历书时与相对论力学时。不过在计算机中用的时间标准现在大多都是原子时。
时钟仪器
有了时间标准,如何通过仪器去实现精确记录时间的功能?这又是一个漫长的历史发展过程,这其中时钟仪器经历了以太阳影子、水流、燃点线香、沙漏、以砝码带动的机械、钟摆、电子、石英甚至原子等方式的发展。各种方式的尝试,只是为了找到能更精确记录时间的方式。目前的原子钟就是人类现在科技能实现的最精确的记录时间的方式。计时方式越来越精确,也反应了社会越来越快的节奏。
时钟同步
有了精确的时间源,如何给网络上的计算机设备同步时间?这方面有以下几种协议:
PTP(Precision Time Protocol):精确时间协议(PTP)是一种用于在整个计算机网络中同步时钟的协议。在局域网中,时钟精度达到亚微秒级,适用于测控系统;
NTP(Network Time Protocol):网络时间协议(NTP)是在互联网上进行时钟同步的一个网络协议,位于OSI模型的应用层。具体可看这篇 计算机的时钟(一):NTP协议 的介绍;
GPS(Global Positioning System):通过GPS来实现高精度的时间同步;
北斗授时:和GPS一样可提供高精度的时间同步;
TrueTime:Google设计的用来给其分布式数据库Spanner精准同步时间的算法。相比NTP协议,误差小很多。具体见这篇 计算机的时钟(四):TrueTime 的介绍;
计算机或智能设备大多采用NTP协议来同步时间。你还可以访问 time.is 这个网站来查看你的设备与高精度原子钟的时间偏差:
时间表示
时钟表示时间的方式很简单,但计算机在内部如何表示时间?有三个常用时间格式的标准:
ISO 8601:在Linux中输入 date –iso-8601=ns 会得到ISO 8601格式的时间 2021-12-25T19:31:12,753290000+08:00;
RFC 3339:在Linux中输入 date –rfc-3339=ns 会得到RFC 3339格式的时间 25 December 2021 19:31:10.900897000+08:00;
Unix timestamp:在Linux中输入 date +%s 获得当前时间的Unix时间戳 1640432950。它是自Unix纪元(Unix Epoch:1970-01-01 00:00:00.000000000+00:00)以来的不包括闰秒的到现在的总秒数。需要注意的是Unix时间戳是协调世界时(UTC)标准。
RFC 3339可以理解为是ISO 8601的一个扩展,所以很多时候可以直接用RFC 3339替换ISO 8601。以RFC 3339为例:
也可以用 date –rfc-3339=ns -u 获得协调世界时(UTC)标准的时间格式 25 December 2021 11:45:42.740865000+00:00。任何时间都建议使用RFC 3339的UTC格式或Unix时间戳做时间表示,Unix时间戳在存储和网络传输方面耗费资源更少:
时钟类型
大多时候我们不会意识到计算机在与NTP服务器同步时间时的一些影响,但在某些场景比如统计某个程序函数的执行时间时,时间同步会让计算机的本地时间突然向前或者回拨,当程序在做时间差统计的时候,可能会带来错误的值(比如负数),这是墙上时钟(Wall Clock)的弊端。
而单调时钟(Monotonic Clock)则是一种更简单的时间表示方式,它不会被NTP服务器影响,但是它也不能保证时间的精确性,因为它只能保证时间的递增或递减,而不能保证时间的绝对准确。
在Java中System.currentTimeMills()API返回的就是墙上时钟时间,System.nanoTime() API返回的是单调时钟时间,后者可以用来在日志中统计程序函数执行时间。
分布式系统中的时间
重新认识程序
什么是程序?估计很多人会回答 程序 = 算法 + 数据结构。但这只从静态的角度描述了程序,而程序的价值是在运行的时候。如果从运行时的角度看呢?Leslie Lamport 在这个 Programing Should Be More Than Coding 课程中给出了他的解释:
一个程序可以看作一系列行为的集合,而一个行为又可以看作一系列状态的转换。这个和天然分布式的 Erlang 的世界观有异曲同工之妙:
在Erlang的世界观里,程序是由一系列独立的进程组成的,这些进程通过消息传递(Message)来通信共享状态。消息传递的是什么?消息传递的是状态,而状态的转换是由进程来完成的。这个消息传递就是上述中的行为,从这个角度看,程序是由大量的消息传递行为(事件)组成的。
因果一致性
在单节点的程序里,有全局的墙上时钟,大量的消息传递行为并不会出现因果关系的错误。请看如下的例子:
如上图,当进程P2收到P1发来的消息m1后,经过处理之后P2将m2发送给P1和P3。因为全局时钟的存在,t1 < t2,所以在P3进程的视角看,肯定是先收到一个P1发来的m1,然后再收到一个P2发来的m2。实际上,我们程序的逻辑也是P2收到m1后经过处理才生成m2,于因果关系上,先有m1,然后才有m2。不可能出现P3先收到m2,然后再收到m1的情况。
举个实际的例子就是,小王(P1)发了个朋友圈,说“天好美”(m1),小李(P2)看到后评论了一句“好美!”(m2),小赵(P3)看到的肯定是先小王发的,然后小李评论的,不太可能出现先看到小李的评论,然后再看到小王的朋友圈的情况。
但在分布式系统中,因为网络原因和节点同步时钟会产生一定的偏差,可能会出现因果悖论。请看如下的例子:
和上面的例子的区别在于,此时的进程分布在不同的节点,甚至可能在物理上相距很远。因为时钟无法做到极高的精确性,此时可能会在不同节点上出现t2 < t1的情况。站在P3的视角,它很难判断到底是先有m1还是先有m2:
虽然先收到m2,后收到m1,但完全可能是网络延迟,导致m1后于m2;
就算P1发送m1时携带了自己的本地时间戳t1,P2发送m2时也携带了自己的本地时间戳t2。但由于P1和P2的时间并没有完全同步,导致P2的t2 < P1的t1。P3在收到消息后,根据消息携带的时间戳排序,得到了错误的排序,出现了m2先于m1的因果悖论。
放到上述朋友圈的例子就是小赵先看到了小李的评论,然后才看到了小王发的朋友圈😵。
逻辑时钟
靠物理时钟肯定是解决不了分布式因果一致性的问题了,因为现有的技术无法解决时钟在不同节点完全同步的问题。
这时候一位大神跳出来了,从狭义相对论中得到了启示写了这篇深刻影响分布式系统领域的论文:Time, Clocks and the Ordering of Events in a Distributed System。这位大神正是之前提到的Leslie Lamport,他同时也是分布式领域极其复杂的 Paxos 共识算法的作者,也是我们接下来要讲到到的 Lamport timestamp 的作者。
Leslie Lamport在这个 论文背景介绍 中提到:
… Special relativity teaches us that there is no invariant total ordering of events in space-time; different observers can disagree about which of two events happened first. There is only a partial order in which an event e1 precedes an event e2 iff e1 can causally affect e2. …
这段关于狭义相对论的介绍中提到了两个数学概念:全序 与 偏序。简单理解就是宇宙时空中不存在绝对的全序事件顺序(因为很多事件都没有因果关系,也就是没有次序关系)。但当且仅当(iff)事件e2是由e1引起时(存在因果关系),e2后于e1发生,它们之间存在偏序关系。
既然在分布式中很难解决时钟同步的问题,那就不去解决这个问题。在分布式中同步时钟是为了解决不同节点发出事件的次序(因果关系),那只需要通过某种手段解决这个事件次序的问题就好了。这正是逻辑时钟解决问题的思路。
理论有点复杂,让我们先看看Lamport timestamp到底是怎么解决这个复杂的分布式因果一致性问题的吧。
Lamport timestamp
狭义相对论告诉我们宇宙时空中不存在绝对的全序事件,但我们是否可以在有限的分布式节点中构建一个全局的全序逻辑时钟来描述事件顺序?这正是 Lamport timestamp 的解法。
Happened-before
在进行算法介绍之前先介绍Leslie Lamport定义的另外一个重要的概念:Happened-before,它定义了如果事件 ei导致了事件ej,那么一定ei发生在ej之前。它满足如下几个数学规则:
ei和ej是同一个进程内的事件,ei发生在ej之前,则ei → ej;
ei和ej在不同的进程中,ei是发送进程内的发送事件,ej是同一消息接收进程内的接收事件,则ei → ej (发送进程和接受进程可以分布在不同的物理节点上);
如果ei → ej并且ej → ek,则ei → ek (满足传递性)。
如果两个事件无法推导出顺序关系的话,我们称两个事件是并行的,记作ei|| ej。
第4条规则说明Happened-before是一种偏序关系。
看到这里熟悉Java的人立即意识到在Java JMM 中就有了Happened-before的概念:在多线程编程中,Happened-before提供了跨线程的内存可见性保证。在编译器执行编译优化的时候,会对指令做重排序,为了保证程序运行的最终正确,JMM设定了一些Happened-before规则,程序员只要遵循这些规则,JMM就能保证程序的正确运行。
所以Happened-before是一种约束,遵循它我们能获得一定的事件次序最终保证。
算法描述
发送进程端:
time = time + 1;
send(message, time);
接受进程端:
(message, time_stamp) = receive();
time = max(time_stamp, time) + 1;
这个算法描述起来就是:每个节点都维护一个本地永远递增的逻辑时间戳(初始都为0),节点发送事件时必须携带此逻辑时间戳。每当节点有事件产生或接收时,节点本地的逻辑时间戳就自增1。当节点接受到一个事件,如果事件逻辑时间戳比本地逻辑时间戳还小就忽略,否则就接收此事件,并更新节点本地的逻辑时间戳,新的值取节点本地逻辑时间戳和接收事件所携带的逻辑时间戳二者的最大值并自增1。
一个例子
如上图所示,我们以m@(T,P)的模式全局识别一个事件m,比如m1@(1,P1)代表在P1进程上发生了事件m,在P1上本地逻辑时间戳为1。接下来事件的发生如下描述:
在P1进程上发生了事件m1@(1,P1),之后P1给P2和P3发送了此事件;
P2先接收到了此事件,本地更新逻辑时间戳,这个事件在P2中为m1@(2,P2)。之后P2更新了m1,本地变为m1@(3,P2),并将此事件传播给P1和P3;
在P1中又对m1进行了两次更新,在P1中m1为m1@(3,P1)。之后P1收到了P2发来的m1,因为接受的事件为m1@(3,P2),但本地的m1为m1@(3,P1)。两者的逻辑时间戳都一样大,此时是难以区分到底选择哪个版本的m1,因为无法确定这两个版本m1的因果关系;
在P3中先接收到了P2发来的m1@(3,P2),因P3之前并无m1,所以接收此事件并更新为m1@(4,P3)。之后P3收到了P1发来的m1@(1,P1),因为接收事件的逻辑时间戳小于P3本地的逻辑时间戳,所以忽略了此事件。
通过上述描述,我们解决了在 因果一致性 中描述的P3无法区分从P1和P2发来两个版本m1的问题。
局限性
如上述第3步的描述,Lamport timestamp算法无法识别并行事件。在P1的视角里它无法确定是选择自己本地版本的m1还是P2发送的m1,因为其可能是并行事件,毫无关系,也可能是Happened-before的因果关系。
从序论的角度分析,是因为Lamport timestamp通过构造一个全序关系来描述实际上是偏序关系的问题集。如果两个事件毫无关系,它们的逻辑时间戳的大小比较毫无意义。
不过在上述例子中,P1最终选择了通过比较事件的节点大小来确定是否接收事件。在这里我们的冲突解决策略是如果同一事件相同逻辑时间戳,我们选择节点小的值的事件,也就是P1 > P2 > P3。
Vector clock
Vector clock 解决了Lamport timestamp无法识别并行事件的问题。通过构造偏序关系来解决偏序关系的问题集。
Vector clock在Lamport timestamp单值逻辑时间戳的基础上升级为向量(Vector)时间戳。
有了向量的大小比较,Vector clock将全序改造为了偏序关系,这样就可以识别出事件是否为并行还是Happened-before的因果关系。
如在P3收到P1发来的事件m1@([1,0,0],P1)时,本地已经有了m1@([1,2,1],P3),因为[1,2,1]比[1,0,0]大,所以忽略了此事件。
但在P1收到了P2发来的事件m1@([1,2,0],P2)时,本地已经有了m1@([3,0,0],P1),但[1,2,0]与[3,0,0]不可比较,或者说这两个向量平行。此时冲突发生,需要选择一种冲突策略来确定最终接受的m1版本。
局限性
Vector clock虽然解决了Lamport timestamp无法识别并行事件的问题,但它也不是一种公平的算法。在冲突出现时,也只能选择提前定义好的冲突策略来确定最终的事件版本。不过在分布式中集群一般会通过共识算法选举一个leader节点,通过leader节点可以制定一些更合适的冲突解决算法。
另外一个问题在节点数量多的时候,向量会变的很大,网络通信及向量的比较计算都会对系统产生一定的资源损耗。而且也没有考虑节点的动态加入或退出对向量比较的影响。
冲突策略
识别冲突后需要一种冲突策略来解决冲突。这里我们可以选择在Lamport timestamp算法中提到的用节点大小比较来确定最终版本,也可以选择 混合逻辑时钟(Hybrid Clock ) 算法。
应用场景
逻辑时钟在分布式系统应用场景非常广泛。比如 构造分布式锁、分布式节点状态同步(Replication)、数据冲突检测、强制因果通信、CRDT 及 实时协作应用(Collaborative) 等。
限于篇幅及能力,这些内容请读者自行查阅研究。
结语
本文通过梳理在现实世界中时间在分布式系统应用会产生的一些问题,介绍了通过引入逻辑时间算法来解决这个问题。可以把逻辑时间理解为分布式系统的一个基石理论,因为当你去研究分布式系统的很多算法,都会发现它的影子。
最后的最后,时间究竟是什么?相信不同人有不同的答案,但当你思考它时,就会发现的影响无处不在。
参考文章&&进阶阅读
Time, Clocks, and an Implementation in Erlang
Distributed Systems 4.1: Logical time
逻辑时钟-如何刻画分布式中的事件顺序
Lamport逻辑时钟(Lamport Timestamp)和Vector Clock简单理解
计算机的时钟(二):Lamport逻辑时钟
从物理时钟到逻辑时钟
Causal ordering
Data Laced with History: Causal Trees & Operational CRDTs
antonkharenko/logical-clocks: Lamport and Vector clocks
gsharma/vector-clock: Vector clocks & Lamport timestamps
从相对论到区块链:论分布式系统中的时间
更新时间: 33个月前 版本: 529067c0f
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
SQLite的文艺复兴
Real-time Web应用开发新体验
走进Web3
分布式系统下的认证与授权
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
分布式系统下的认证与授权
18 December 2021 29:18 | 6447 字 | 10658 次阅读
分布式技术
认证 • 授权 • 凭证 • 分布式系统 • JWT
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
在软件系统设计中,如何让应用能够在各种环境中安全高效的访问是个复杂的问题,这个问题的背后是一系列软件设计时需要考虑的架构安全问题:架构安全性|凤凰架构
认证:系统如何识别合法用户,也就是解决 你是谁 的问题;
授权:系统在识别合法用户后,还需要解决 你能做什么 的问题;
凭证:系统如何保证它与用户之间的承诺是双方真实意图的体现,是准确、完整且不可抵赖的;
保密:如何安全的持久化用户的账户信息,确保不会被任何人窃取与滥用;
传输:在复杂的用户环境中,如何安全的传递用户信息,保证不被第三方窃听、篡改和冒充。
在漫长的架构演进历史中,业界对这些问题已经有很成熟的解决方案。在架构安全这块,最好的是遵循技术标准与最佳实践,尽可能不重复造轮子或“创新”。下面这个思维导图就是针对这些问题的常见的技术标准及方案:
TOTP(基于时间的一次性密码算法)
Twitter Basic
Windows Live ID
AWS4-HMAC-SHA256
OCRA(质疑/应答算法)
HOBA
Bearer
Digest
HTTP Basic
WebAuthn
RFC 6238
表单认证
扩展方案
RFC 6287
RFC 7486
RFC 6750
RFC 7616
RFC 7235
验证
签名
核验公钥
CA 认证
公钥分发
生成密钥
加密算法
Web认证
HTTP
SSL/TLS
Spring Security
Shiro
JAAS
Cookie-Session
JWT
客户端模式
密码模式
隐式授权模式
授权码模式
ABAC
RBAC
ACL
OIDC
SAML
CAS
Kerberos-based
通信内容上的认证
通信协议上的认证
通信信道上的认证
Java
引用令牌
自包含令牌
OAuth2
访问控制
SSO
方式
实现
凭证
授权
认证
架构安全
在研究分布式系统的认证和授权问题前,让我们回到单体架构的时代,看看在单体架构上这些问题是如何被解决的。
单体系统
认证
认证主要解决 你是谁 的问题,从方式上来看有以下三种:认证|凤凰架构
基于通信信道:建立通信信道之前需要证明 你是谁。在网络传输(Network)场景中的典型是基于SSL/TLS传输安全层的认证。
基于通信协议:在获取资源之前需要证明 你是谁。在互联网(Internet)场景中的典型是基于HTTP协议的认证。
基于通信内容:在提供服务之前需要证明 你是谁。在万维网(World Wide Web)场景中的典型是基于Web内容的认证。
在单体系统时代,认证方式一般是在通信信道上开启HTTPS,在通信协议上利用 HTTP Basic/Digest/Bearer/HOBA/OCRA 等方式并在通信内容上结合表单或 TOTP 等的认证组合方式。这样可以从通信的不同阶段获得相应的安全保证。
如果想对基于HTTP协议的认证方式做进一步的了解,可以参考这两篇文章:
认证|凤凰架构
细说API -认证、授权和凭证- Thoughtworks洞见
单点登录(SSO)
认证的一个常见应用场景是单点登录。单点登录主要解决了一个一次登录访问多个独立应用的问题。在单点登录方案出现之前,每个应用都需要独立登录维持各自的会话。相关的技术方案已经很成熟,主要有以下:
Kerberos-based:MIT设计的SSO协议,基于对称密码学,并需要一个值得信赖的第三方。其广泛用于操作系统认证,如被Windows 2000和后续的操作系统作为默认的认证方法。
CAS:Yale设计的SSO协议,基于浏览器的SSO方案,部署简单,适用于简单的应用场景。
SAML:基于XML标记语言的认证断言方案,适用的场景众多,但技术较复杂。
OIDC:在OAuth2的基础上额外加一个JWT来传递用户信息。功能全面强大,是目前很流行的SSO方案。
授权
授权主要解决 你能做什么 的问题,从方案上来说有以下几种:
ACL:访问控制列表(Access-control list)广泛用于操作系统内部的文件系统、网络及进程权限控制方面。如在Linux中,可通过 getfacl 获取目录的默认ACL设置。
RBAC:RBAC通过将权限属性从ACL方案中的单个用户抽取成更为抽象的角色(Role),通过给角色一组权限属性,再将多个角色赋予某个用户,实现了比ACL更为灵活强大的权限控制方案。实际上大部分系统的授权方案采用RBAC就足够了。但RBAC在面临复杂的权限控制需求时可能面临角色爆炸的问题,这时可以考虑采用更细粒度的ABAC方案。
ABAC:ABAC是比RBAC更细粒度的权限控制方案。通过引入一组称为“属性“的特征,包括用户属性、环境属性和资源属性。例如,ABAC可以对用户的访问做进一步的控制,如只允许在特定的时间或与相关员工相关的某些分支机构进行访问员工信息的操作,而不是让某部门的人员总是能够访问员工信息。但ABAC的问题在于初始设置需要定义大量的属性,工作量比RBAC要大。一种实现ABAC的技术标准是XACML,它通过定义了一些抽象的组件如策略执行点(PEP)与策略决策点(PDP)及其他一些组件完成细粒度的权限控制。
OAuth2:OAuth2是为了解决应用系统给第三方系统授权的问题而设计的授权框架。传统的客户端服务器交互模式中,客户端持有资源访问凭证(如用户名密码),服务端验证成功后放行。而在给第三方系统提供资源时,如果给第三方系统资源凭证,可能会带来未知的安全问题,比如凭证泄漏,凭证回收等问题。如果应用系统需面向第三方系统提供服务,那需要使用此方案。同时因为OAuth2做授权的时候一般需要用户登录,也能实现单点登录的功能。
如果想对授权做进一步的了解,可以参考这篇文章:
授权|凤凰架构
凭证
凭证是为了解决在认证授权后如何承载认证授权信息的问题。在单体应用时代,主流的解决方案是基于HTTP协议的Cookie-Session机制为代表的服务端状态存储技术。
由于HTTP协议本身是无状态的,要维持一个会话(Session),而不是每次访问都重新认证授权,需要客户端也就是浏览器通过Cookie来存储服务器端返回的一个凭证信息,这个凭证信息一般是一串随机的字符串,用来代表用户此次的会话标识。每次请求浏览器都会在HTTP Header中携带这个Cookie信息,应用拿到这个会话标识后从内存或缓存(Cache)中查询出用户的信息,这样就定位到了具体的用户,实现了会话的维持。
这套古老的方案存在以下先天优势:凭证|凤凰架构
状态信息都存储于服务器,只要依靠客户端的 同源策略 和HTTPS的传输层安全,保证Cookie中的键值不被窃取而出现被冒认身份的情况,就能完全规避掉上下文信息在传输过程中被泄漏和篡改的风险(但Cookie方案容易受到 CSRF 攻击,这种可通过 CSRF Token 技术防御);
另一大优点是服务端有主动的状态管理能力,可根据自己的意愿随时修改、清除任意上下文信息,譬如很轻易就能实现强制某用户下线的这样功能;
服务端也很容易实现如统计用户在线这类功能;
一切都很美好,直到我们来到了分布式系统时代。
分布式系统
分布式系统与单体系统的一大区别就是状态管理。分布式系统通过把单体系统中有状态的部分转移到中间件中去管理,从而很容易做到水平扩容,提高系统峰值处理能力。在架构认证和授权部分,分布式和单体并没有什么不同,唯独有变化的在持有状态的凭证部分。
我们知道单体应用在服务端管理用户会话信息,客户端只持有会话标识。如果服务端要将此用户会话状态转移出去有两种处理思路:
将用户会话信息继续托管至服务端。此时有几种服务端方案可以选择:
中心化存储:转移到中间件如Redis中去。利用Redis 极高的并发处理能力,也可以做到弹性横行扩容。不过可能会带来中间件高可用性维护难的问题,通过租赁云服务商的托管中间件是降低中间件 单点故障(SPOF) 的一种方式;
会话复制(Session replication):让各个节点之间采用复制式的Session,每一个节点中的Session变动都会发送到组播地址的其他服务器上,这样某个节点崩溃了,不会中断该节点用户的服务。但Session之间组播复制的同步代价高昂,节点越多时,同步成本越高。
会话粘滞(Sticky session):通过负载均衡算法如Nginx的 IP Hash 算法将来自同一IP的请求转发至同一服务。每个服务节点都不重复地保存着一部分用户的状态,如果这个服务崩溃了,里面的用户状态便完全丢失。
为什么在分布式系统中共享状态就这么困难?这是因为分布式系统中有一个不可能三角的理论:CAP。这个理论简单的理解就是因为在分布式系统中,因为网络无法做到绝对的可靠(分区容错性:Partition Tolerance),只能在一致性(Consistency)和可用性(Availability)间选择一个。
比如上述的三种服务端方案其实都是牺牲了CAP的某个方面。比如第一种中心化存储方案我们放弃了中心化存储的分区容错性,一旦其网络分区,整个集群都会不可用。第二种会话复制方案我们牺牲了可用性,当节点在同步会话数据时,整个服务会短暂的不可用。第三种会话粘滞方案我们牺牲了一致性,一旦某个节点宕机,整个集群的数据会因该节点的数据丢失而达到不一致的状态。
将状态从服务端转移到客户端。Cookie-Session是一种引用令牌(Reference tokens),也就是客户端持有的是服务端存储的会话引用标识。还有一种自包含令牌(Self-contained tokens),如 JWT 就是这种客户端保存会话信息的技术,服务端只是去校验会话信息是否合法。
JWT
如果你对JWT不了解,可以先看这两篇:
JWT |凤凰架构
The Hard Parts of JWT Security Nobody Talks About
由于JWT的Payload并未做过多限制,所以很容易产生滥用的问题,并且带来很多误解。 比如下面的一些问题:
误把JWT当作Cookie-Session使用(把JWT当作引用令牌使用),会带来未知的隐患。遵循不重复造轮子和“创新”的指导原则,尽可能不要这么做;
认为JWT更安全。虽然JWT采用了一定的加密算法签名,使其具备了抗篡改的能力。但其Payload大部分都只是采用 base64UrlEncode 编码,数据并不是加密的。攻击者可以通过 会话劫持(Session hijacking) 技术拿到JWT会话信息,之后通过 会话重放攻击(Session Replay Attack) 获取用户资源,所以最佳实践是通过启用TLS/SSL来加密通信信道。
把JWT存储到浏览器的Local Storage中。此方式很容易受到 XSS 攻击导致JWT泄漏。可通过服务端启用 内容安全策略(CSP) 来防御这种攻击。
采用对称加密方式签名(Signature)。对称加密密钥一旦泄漏,会让整个服务的基础设施遭受安全威胁。JWT支持非对称加密算法,只有签名的服务需要私钥,其他验证JWT信息的服务只需要使用公钥即可。
不校验JWT的签名算法。这篇 Critical vulnerabilities in JSON Web Token libraries 文章提到JWT的一种漏洞,通过 none 算法规避令牌验证。所以最好每次都验证JWT header中的签名算法是否是期望的。
相信看了上述的一些问题,你对JWT的简单、安全有了新的理解。这还没完,JWT还有以下一些Cookie-Session没有的问题:
令牌难以主动失效:JWT中虽然有 exp、nbf 与 iat 这些和时间相关的属性,但很难在令牌到期之前让令牌失效,比如很难在用户退出登录时立刻让签发的令牌全部失效。虽然可能通过一些“黑名单”的技术解决这个问题,不过相比Cookie-Session来说,引入了一定的复杂性;
令牌数据老旧:很难把签发的令牌全部更新成最新的数据。比如把用户的权限信息(Role)放在JWT Payload中,当用户的角色发生变化时,很难把之前签发的令牌信息更新成最新的数据;
令牌存储:存储在客户端意味着有多种选择:Cookie?Local Storage?如果放在Cookie中,为了安全,一般会给Cookie设置 http-only 和 secure 的属性。但这也会带来一定的不便性,比如客户端要读取JWT Payload的内容只能借助服务端API接口。如果将JWT存储至浏览器Local Storage,虽然方便了客户端读取,但可能会带来XSS攻击的威胁,又需要去设置CSP来防御这种威胁;
令牌大小:JWT相比Cookie-Session还是大不少,尤其是要在Payload中存储一些额外的权限信息。一般服务端都有对HTTP Header的大小限制;
网络开销:更大的文本意味着更高的网络开销,进一步会需要更复杂的基础设施,也会产生复杂的运维问题等;
难以统计:服务端无状态意味着很难做诸如统计用户在线数量的功能;
JWT解决了Cookie-Session方案在分布式系统中因CAP的限制而带来的问题,但同时也带来了一些新的问题。所以并不能说JWT就是Cookie-Session在分布式系统中的完美替代。
那么JWT的最佳使用场景到底是什么?这篇 Stop using JWT for sessions 给出了以下的结论:JWT更适合作分布式系统中的一次性令牌使用。分布式系统继续使用Cookie-Session做会话管理,但可以在认证鉴权后生成JWT做分布式系统内部服务调用间的一次性令牌。
让我们通过一个例子来理解下在分布式系统下的认证授权场景。
一个例子
此处Auth服务承担的是授权(Authorization)的职责,而不是认证(Authentication)的职责;
OAuth2在协议中是做授权框架的,但是其一般需要登录授权,也能实现SSO的功能。
用户通过HTTPS访问我们的应用。当请求发送至微服务网关层(Gateway),网关检测HTTP Header中的Cookie发现没有 SESSIONID 这个键值对,重定向至SSO登录页面。
用户通过SSO登录我们的应用。
用户信息存放至AD/LDAP等系统中。管理员提前给用户配置好角色权限。
SSO集成方案我们选择OIDC。OIDC集成了AD/LDAP,当用户提供正确的用户名和密码后,SSO重定向至网关。
网关生成了 SESSIONID 键值对并通过HTTPSet-Cookie响应给用户浏览器设置了此Cookie。
浏览器重新发起带SESSIONIDCookie的请求。网关经过查询其缓存或中间件(如将会话信息存放至Redis)中的Session信息确认了用户的身份信息。之后网关请求Auth服务利用其私钥签名生成JWT凭证,JWT Payload中可以存放一部分用户信息和角色信息,这些信息可以从中间件中或AD/LDAP中查询出。
网关之后将此JWT凭证通过反向代理转发至内部的BFF服务,之后请求到达内部的领域微服务。
各领域微服务接受到请求后,先从HTTP Header中拿出JWT凭证。
在执行真正的业务逻辑前,先利用之前定时从Auth服务中同步获取的公钥。
Auth服务通过一个类似 https://<your_domain>/.well-known/jwks.json 的API提供JWT公钥的分发。关于 .well-known 前缀,可阅读 RFC 5785 做进一步了解。在 jwks.json 文件中,我们可以找到 JWK 或JSON Web Key,这是我们用来验证签名的公钥。
校验JWT这块逻辑属于微服务共有的部分,一般可以开发一个SDK包来做这个通用的工作。为了提高性能,可使用缓存技术,定时从Auth中同步公钥。
获取到公钥后验证成功后拿出JWT Payload即可获取到用户信息和角色权限。
全部流程就是这样,我们得到了以下的一些好处:
这个流程里我们并没有将JWT返回给用户,只是在认证授权过后生成一个一次性的JWT令牌凭证用于微服务内部服务间的调用。因为用户的权限信息存放至JWT Payload中,内部的服务并不需要从AD/LDAP中获取用户权限信息。可能有人觉得内部服务直接从中间件中获取用户会话信息也可以,但这又让我们的应用进一步耦合了中间件,同时也让一个请求链路中产生更多的子请求,不如直接在请求头中存放用户信息的方式高效。
在微服务内部间传递的是经过非对称加密算法签名的JWT凭证,并不是一个JWT Payload信息。就算我们的微服务内部被入侵,攻击者也并不能通过篡改凭证中用户的权限信息来搞破坏。这也满足了分布式系统中 零信任网络(Zero Trust) 的部分要求。
与外部第三方应用的通讯(M2M),可以采用OAuth2的方式或Personal Access Token这种方式来集成。
通过引入SDK与定时同步公钥的机制,我们引入了一定的复杂度。比如SDK在异构编程语言的项目中开发复杂的问题。不过这个问题在云原生系统时代有了不同的解法,让我们之后讨论这个问题。
架构总是在演进,也许分布式系统中很多问题我们还没完全解决,就来到了云原生时代。
云原生系统
如果你对云原生应用开发还不了解的话,可以先看看我这篇 K8S云原生应用开发小记。云原生系统其实并不是什么后分布式系统时代。它们两者都是为了解决不同场景的问题而出现的解决方案。
在认证授权这块,云原生系统的优势在于可以通过 服务网格(Service Mesh) 做一些业务系统中通用的切面工作,比如我们在分布式系统中遇到的校验JWT的SDK其实就可以放入服务网格中的边车(Sidecar)去实现,让业务应用更专注特定领域的业务。
由于这篇文章并不主要讨论云原生,对这部分感兴趣的可以参考以下两篇文章做进一步了解:
Service Mesh架构下的认证与授权
微服务下的身份认证和令牌管理
总结
由于篇幅及能力限制,这篇文章我只能从高层次梳理在不同架构演进中认证、授权及凭证这些和架构安全相关的技术的发展过程。由于这些技术涉及了大量的技术标准及实践,很难在一篇文章中对这些技术做详尽的分享,更无法去分享如何实现。但有了这些理论支持和最佳实践,希望能让你在实现的过程中多了一个指引。如果你想进一步了解,可参考文章中的参考文章链接。
最后,技术总是在不断的发展,但并不是新技术总比老技术“先进”。正如文章中对Cookie-Session与JWT的分析对比,技术方案总是充满了各种 Trade-off。而作为一个工程师,我们能做的就是认清这些技术的历史背景及局限性,选择最适合项目需求的技术方案。
更新时间: 22个月前 版本: 3259874e5
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
SQLite的文艺复兴
Real-time Web应用开发新体验
走进Web3
分布式系统中的时间
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
BMPIGitHubTwitter学习技术投资|English
如何学习一门技术
20 November 2021 12:27 | 2740 字 | 26179 次阅读
个人成长
如何学习 • 技术方法论 • WebRTC
点我在线阅读《个人产品开发指南》,教你从零到一快速开发自己的个人产品。
品牌赞助
要学否
怎么学
主动搜索
技术标准
技术历史
做好笔记
学习计划
学习技巧
寻求帮助
心理建设
怎么用
作为一个 终身学习 的实践者,我经常有学习一些新技术的需求。如何学习这些新技术不同人有不同的做法,早前我也写过一篇 如何快速学习一项新技能? 的文章分享我学习的理论框架,但这篇文章我会以我学习WebRTC这个技术为例分享我在学习新技术时用的一些方法。
如何学习某个知识,在我看来,主要矛盾在于解决这三个问题。
要学否
在学习一门新技术前需要解决的第一个问题是要不要投资时间去学这门技术。就像买书最大的成本并不是买书的价格,而是看书的时间。花费大量的时间去看一本没有价值的书,无异于浪费生命。正是方向搞错了,越努力越尴尬。
怎么确定一门技术的价值,可以从以下两个方面来考虑:
从知识体系出发:某门技术经常不是孤立存在的,而是一个积木般搭建的大厦的一部分。要学习顶部的技术,就需要掌握一定的底部技术。如果一个技术很基础很底层,被很多高层的技术所依赖,那学习这门技术就很有价值。
从应用前景出发:如果一门技术很有市场“钱景”,或者有潜在的市场需求,那学习这门技术就很有价值。毕竟我们学习目的很大的一部分在于赚钱解决自己的生活问题。
不过这里的难点在于从我们已知的信息来分析,很难判断某门技术的市场前景。如果分析判断错误,很可能会导致我们学习这门技术的时间被浪费。那最佳的选择的就是尽可能让自己所学的技术都满足这两点,哪怕最后没有市场前景,但如能成为我们知识体系的基础,也值得投入时间去学习。
基于这两方面的考虑,我开始学习了分布式系统的一些底层知识:
之后通过搜索间接找到了基于WebRTC技术的语音聊天网站 speakrandom,在分析这个网站技术栈的时候找到了 pion/webrtc 这个框架,最终决定从这个框架入手开始学习WebRTC。
怎么学
在制定了学习目标之后,剩下的问题是怎么怎么学?学习方法千万条,重要的是找到适合自己的学习方法。
我的方法是善用搜索,找到对的资料和对的人。学习本身不应是一件复杂的事情,因为它不是做研究,不是探索未知的东西,只是站在巨人的肩膀上把已经被解决的问题学习一遍。
但这里的难点在于资料千万份,一不小心就找到错误的资料,让本来简单的学习变得复杂,这就像天龙八部鸠摩智学了段誉给的错误的六脉神剑剑诀,很容易学的走火入魔。
主动搜索
主动获取资料 方式的要点在于从错误少的信息库筛选、交叉对比选择要看的资料。由于很多技术资料都是用英文写的,用Google英文搜索更容易获取高质量的资料。另外使用Google图片关键词搜索可以快速获得架构方面的资料,方便从高层次理解这个技术。
一般我会从Google、YouTube和GitHub上搜索某个技术相关的资料、视频教程和开源库。以搜索切入,找到合适的开源项目或者技术标准,然后制定学习计划。很容易通过 webrtc 关键词在这些平台上搜索得到这些资料和教程:
GitHub
https://github.com/pion/webrtc
YouTube
WebRTC Crash Course
Google
Build the backend services needed for a WebRTC app: STUN, TURN, and signaling - HTML5 Rocks
从pion/webrtc这个库上了解到作者是 @Sean DuBois,GitHub关注一波然后去YouTube搜索下他的演讲,又收获了一波高质量的教程:
技术标准
另外一个高质量的资料是协议标准,比如IETF RFC文档。搜索一番后找到WebRTC相关的标准:
https://www.w3.org/TR/webrtc/
从这个W3C制定的标准里又可以看到很多IETF RFC的资料。了解这些技术标准有助于我从高层次理解这个技术的一些特性。当然这些标准的细节我暂时不会去看,等到需要了解细节的时候再去看。
另外还可以从标准中梳理出这个技术的一些历史背景知识。
技术历史
复杂的技术不是横空出世的,而是从简单的技术逐渐根据需求而演变来的。很多时候一个技术的复杂是因为其有很多历史性而导致的,比如Java的范型之所以使用复杂并具备很多限制性是因为其为兼容老的库而妥协设计出的产物。了解这个技术的历史背景有助于降低理解这个技术的复杂度。
做好笔记
搜索而来的资料如果不做整理和记录的话,时间久了就全忘了。我把这些资料整理到了 Logseq 这个双链笔记中。
从下面这个笔记拓扑图中可以看出我记录的分布式知识(Distributed System)和WebRTC间的关联关系。众所周知,学习在大脑的体现就是神经元突触之间建立新的连接,笔记间的知识通过这种方式也能帮助我们快速建立知识间的联系。
学习计划
记录完笔记后,我要做的就是规划时间把整理得来的资料学习消化。在这个环节可用 时间管理 的方法制定该项技术的学习计划。
学习技巧
学习技巧千万条,但有一条是我觉得很重要的,那就是把你所学的说给别人听,从别人的反馈中了解自己对该知识掌握薄弱的点。很多时候大脑在学习的过程中会有很多模糊不清的点,如果不说出来的话,这些不清楚的点会被忽略掉,但如果要让别人听得懂,那需要我们懂的更多才行。
写文章其实也是说给别人听,只不过比单纯的说要更为系统。所以我一般在学习某个技术的时候会去写文章分享。一方面让自己的知识梳理的更清晰,另外一方面可以与读者交流,掌握更多的知识,这也可以解决掉那个经典的我不知道我不知道的知识,当我写出来时,会有看到的人帮我发现我不知道的知识。
当然也可以在社交网站上分享一些学习中梳理的知识点,之后方便整合成文章:
寻求帮助
找对的人解决学习中的困惑无异于能加速整个学习过程。这方面很多开源项目都有自己的讨论区,比如我在理解WebRTC SFU的过程中就有很多困惑甚至错误的理解,在社区中与作者沟通后才得到了正确的答案:
当然我们还可以在论坛、GitHub Issue、邮件组或交流群等地方中寻求帮助。
心理建设
学习里的一大难点可能是不好意思说出自己不懂的点,尤其是工作多年后,要承认自己不懂是件困难的事情。但如果你以终身学习为目标,那么这方面就没什么障碍。不懂就去学,不懂就去问。无知并不可怕,年龄大不懂也不可怕,可怕的是不懂却隐藏这一点。
怎么用
在掌握了技术的理论后,可以通过技术的实践来提高自己的技术水平,比如做一个开源项目。对技术的应用有两种方式:
从零开始,一步步实现自己的系统。这种方式的问题在于,刚开始我们对技术的应用不是很熟悉,完全自己做可能无法应用一些最佳实践,摸着石头过河的成本比较高。
从现有的应用中改造。开源项目有很多好的应用,可以直接用来学习并改造。一方面可以加速应用的开发,另外还可以学习别人成熟的经验。
通过一番搜索,我找到了两个不错的学习项目:
webtrc-voice-chat
kraken
这两个都是基于pion/webrtc库开发的语音聊天网站。基于这两个开源项目,我可以逐渐学习并开发自己的开源项目。
在学习完这两个开源项目(读完源码)后,我制定了基于WebRTC的应用开发目标:做一个隐私与本地优先的语音聊天的项目:free4.chat。
这个目标有点大,我会把这个业余项目作为技术试验田,把需要学习应用的技术都应用到这里。
学以致用,是学习的最终目的。只有真正去用这个技术,才能真正掌握它。否则花费时间去学习,不用的话很快就忘了。
最后,能看到这里的话,希望这篇文章里提到的一些方法能让你更快速的学习某个领域的技术。
更新时间: 35个月前 版本: 8171f612c
本作品采用知识共享署名-非商业性使用-禁止演绎4.0国际许可协议进行许可
相关文章推荐
从技术难题中学习
Share on Twitter · Discuss on Twitter
策引 ⚡️ i365 ⚡️ 开户 ⚡️ 演讲 ⚡️ 周记
© 2019 - 2025
卢曼的卡片盒笔记系统的确能够对常规的笔记法和知识型工作(knowledge work)的方法有相当大的改进。它可以提高你的效率,让你用更少的时间产出更多的成果。它主要从以下几方面改进常规的笔记法:
让我们的想法互相之间更好地建立联系。卡片盒笔记系统的超文本性质是我们能够将所思所想,包括但不限于概念、术语、灵感、对事情的看法、对模型的认识以及模型参数等等建立联系。而常规笔记法,比如将笔记写在笔记本上,我们很难将记录在第一页的想法与记录在倒数第五页的想法建立可以直接跳转的明确的联系。而想法之间的相互联系有助于产生创新的见解(insight),创新不是无源之水无本之木,创新是各种想法互相联系时产生的意想不到的结果。
提高我们的工作效率。卡片盒笔记法通过提供明确的指导来简化我们的工作流程。明确的流程反过来又会减少使用该方法的难度。在使用卡片盒笔记系统来管理笔记的过程中,我们会感觉到效率的提高。我甚至每周都会有两天时间将卡片盒笔记系统作为我的首要任务,让自己沉浸于写卡片盒笔记的 心流(flow)之中。
不再浪费我们的付出。即便你的笔记无法用于当下正在做的事情,你也会未来的项目储备知识,至少会提高你对该主题相关信息的处理深度。因为你可以很轻松的根据笔记之间的联系找到过去记的笔记。
让我们解决更复杂的问题。处理复杂的问题时,你很难一次性将它全部解决。卡片盒笔记法可以让你集中精力处理问题的一小部分,然后再退一步,用全景的眼光去看这个问题。
我们的笔记不会混乱。常规的笔记法,时间长了会变成臃肿不堪乱成一团。而卡片盒笔记系统则会根据你所要解决的问题的自动调整自己的规模。这是卢曼提到的内生长(internal growth),我更愿意将其翻译为有机生长(organic growth,寓意像生命体一样自然生长)。
让写作更容易,更顺畅,更有条理和说服力。写作和思考的一个主要问题是,我们很难长时间保持写作的思绪(line of thought)不断。冥想时尚且很难将注意力集中于呼吸这样简单的事情,更不用说为了写一篇论文而在几周甚至几个月里都思考同一个问题。而Z卡片盒笔记法以帮你牢牢抓住你对问题的各种想法,并让这些想法保持活力不会丢失。
给每条笔记分配一个固定地址
如果你想引用某条笔记,那么这条笔记要有一个固定且唯一的地址,你可以通过这个地址找到这条笔记(为了方便理解,你可以将笔记的地址类比为网页的网址)。在数字时代,如果不是软件开发人员,我们很少思考这个问题。我们习惯于在网络上用关键字进行搜索,搜索引擎会在很短地时间内给我们返回大量结果。然而,当你处理一堆纸质笔记时,你需要一种方法来让你阅读其中任意一条笔记,而且还不能花费太多的时间和精力。卢曼设计了一套巧妙的编号系统来达到这个目的。
卢曼的笔记编号系统
第一条笔记的地址为数字1。如果你想添加一个与第一条笔记无关的笔记,那么将新笔记的地址设为数字2。如果你想写一条与笔记1有关的新笔记,那么你应该以笔记1为起点产生一条分支,此时新笔记的地址被设为1a。如果还有一条与笔记1相关而与笔记1a关系不大的笔记,那么这条新笔记的地址应该被设为1b。如果你想对笔记1a进行补充或评论,那么你应该创建一个地址为1a1的笔记。所以,简而言之,当你继续一个思路时,新笔记地址的应该在旧笔记的的地址最后一位上递增,比如从1a递增到1b;当你想扩充或评论一条笔记时,那么你应该在该笔记的地址后添加一个新的字符,比如从1a到1a1。要实现这样的编号系统,你需要交替使用数字的字母。
作者注:卢曼的编号系统实际上还包括一个斜线/,但是出于教学目的,我们先不考虑它。
卢曼的编号系统带来了两个结果:
让有机生长(organic growth)成为可能。卢曼没有使用超文本这个词,当然,如果他活在今天,可能会用这个术语。这种有机生长正是维基百科的工作方式。如果你想在一段已有的文本基础上拓展,你就从当前页面分出一个新的页面,然后将新页面的内容以超链接的形式注入当前页面。
让连接笔记成为可能。卡片盒笔记系统超文本的非线性特征让笔记之间的连接变得非常简单。卢曼曾在他如何创建卡片盒笔记系统的手册中写道:你把新笔记放在哪里不重要,只要你能连接到它。
他的编号系统让基于纸张的超文本成为可能,对于卢曼来说,使用这套系统付出的时间和精力是可以接受的(别忘了卢曼是个工作狂)。
现在,卢曼有了一个可以冲浪的超文本。但是,他还需要一个类似于搜索引擎的东西,来进入由超文本组成的笔记网络。“从哪里开始?”是卢曼需要回答的问题。他把笔记索引表(register)作为开始的地方,即入口。
卢曼的笔记索引表常被误认为是标签系统(tag system),但卢曼的笔记都没有标签,他也没用标签系统来组织卡片盒笔记系统。
卢曼 卡片盒笔记系统中的第一个笔记索引表
他的索引表是一个纯粹的入口列表(list of entry point),而非标签列表。在索引表中,每一个条目(item)后面的 ID 编号都很少,有时候甚至只有一个。比如“系统”这个条目就只有一个入口。但是当你发现卢曼居然以这个条目为入口,发展了一种“系统理论”,你一定会非常吃惊。登记表只是一系列入口的组成的列表,通往那些最大最重要的笔记集合的入口列表。在找到入口后,他就可以依靠前面提到的链接系统,在笔记中冲浪了。
如果你想完全复制卢曼卡片盒笔记系统的功能,你必须创建一个超文本,然后通过很少量的入口进入某个主题最核心的笔记,从这些笔记开始,你就可以跟随笔记的链接继续前进了。
卡片盒笔记系统是个性化的思考和写作的工具
与其他方法相比,卢曼选用了一种更有生命力的方式(organic approach),来避免 卡片盒笔记系统变得僵硬。他曾撰写的卡片盒笔记系统手册的标题是“与滑盒交流(Communicating with Slip Box)”,这与我们把卡片盒笔记系统看作写作和思考的滑盒本质上是相同的:卡片盒笔记系统是一种有机的,非线性的,甚至像生命体一样活生生的笔记记录方法。
我们先说说卡片盒笔记系统最重要的特点:
它是如同网页一般的超文本;
它坚持笔记原子化的原则(Atomicity);
它是个性化的;
首先,它不是孤立的文本,也不是大量文本的集合(collection),它是某种超文本,是可以相互参考、相互解释、相互拓展、相互利用信息的文本。常规的笔记系统与 卡片盒笔记系统的区别在于,卡片盒笔记系统非常强调形成笔记间的关系,它优先考虑连接(connect),而非收集(collect)。
要成为超文本系统,卡片盒笔记系统需要多个文本或者说是笔记通过超链接(hyperlinks)互相连接。我们把一个单独的笔记叫做 “Zettel”,”Zettel” 在德语中是纸条的意思,它是 卡片盒笔记系统中最基本的组成部分。
每一条笔记都需要一个唯一固定的地址,我们可以通过这个地址来引用它,以及在笔记卡片之间建立连接。这样一个超文本的笔记系统就可以称得上是卡片盒笔记系统了。
其次,卡片盒笔记系统需要遵守原子化的原则(每条笔记包含且只包含一条知识(one unit of knowledge)。为了弄清楚原子是什么,我们可以问一问自己,我们想用原子化的笔记创造的分子是什么样子的时候,你能大概体会什么是原子。
译者注:
我个人的观点是,原子化的笔记就是一条想法(One thought),而卡片盒笔记系统就是由想法组成的想法之网(web of thought)。
thought 这个词翻译成想法是不得已而为之,因为 thought 含义实在太广,从个人经验来看,thought 可能是一个术语(term)、一个实例(example)、一个主意、一个观点等等,我只有将其翻译成想法可能才更贴切。
我们先看一下没有以单条想法为原子的例子。我们常见的书本,它的最小信息单元是页(page),页又组成了节(section)和章(chapter)。我们可以通过页码(address)找到任何的章、节和页,但是你很难通过页码引用(refer to)书中的某条想法。因为这条想法可能分布于书中的任何地方,没有专门的页码指向它。
维基百科也不是思想之网,因为在一个页面内,你通过超链接只能链接到其他页面里的整篇文章或者页面内的章节,无法链接到该页面中的单个的想法。因为,文章和章节有地址(网址),想法是没有被分配地址。维基百科是一部百科全书,每篇文章都包含某个主题的信息。它是一个信息检索工具,而非思维工具(thinking tool)。
相比之下,在卡片盒笔记系统中,直接引用某条想法是可行的。你只需引用某个原子化笔记的地址即可引用某条想法,因为一条原子化的笔记中有且只有一条想法。
卡片盒笔记系统是一个思维工具,因此它需要把单个想法作为基本单位。为了连接 想法,要给每个想法分配一个地址以供引用。用 Zettler 的话说就是:一条笔记,一个想法。
最后,卡片盒笔记系统不是标准化的工业品,它是一个非常个性化的,属于你个人的工具。独自思考和与他人交流是不同的过程,你的卡片盒笔记系统应该是你个人的思考工具。
不过,这不意味着创建一个共享的,基于特定项目的超文本系统没有意义,但这不是本文定义的卡片盒笔记系统。
让我们重新回顾一下卡片盒笔记系统的定义:
卡片盒笔记系统是一款个性化的,用于思考和写作的工具。它具有超文本(hypertext)的特点,让你的所思所想互相连接形成网络。与其他系统不同的是,你创造的是一张由你的想法、看法、灵感、或者遇到的具体的知识组成的思想之网(web of thoughts),而不是孤立的笔记。它强调笔记之间的连接关系,而非把所有笔记堆在一起。
结合之前的内容,你应该对卡片盒笔记系统的定义有了进一步的认识。那么具体该怎么如何创建笔记卡片才能形成属于自己的思考和写作工具呢?
单个笔记卡片的构成
Anatomy of a Zettel
单个笔记或者说 Zettel 是什么样的呢?每条笔记由三部分组成:
唯一标识符:它为你的笔记提供一个明确的地址;
笔记的正文:这是你记录的笔记内容,一般为一段简短的原子化的信息;
参考文献:如果你的内容来源于外部,你可以在每条笔记的底部写上信息来源,如果你记录的是你自己的想法,则留空。
其实就是这么简单,如果你不清楚有没有做对,只需要回头看看这个基础知识。要形成超文本系统,最起码需要给每条笔记一个地址,也就是一个唯一的标识符,当然还有你记录的笔记内容。
唯一标识符
唯一标识符(ID)是创建卡片盒笔记系统的必备条件,只有拥有了唯一标识符,才可以定位到单张笔记卡片。只有拥有这种能力,才可以创建一张能在知识型工作中协助你的思想之网。
通常,给笔记添加唯一标识符的方法有以下几种:
卢曼式的 ID。前面提到,卢曼给每张笔记卡片一个由数字和字母组成的标识符,这种标识符利用层级结构的形式给笔记卡片编号。如果你准备使用纸质的
Zettelkasten,我建议你使用这种方法,因为它能更好的组织你的笔记。下面讲的其他类型的标识符,很难在纸质版的卡片盒系统中取得好效果。需要注意,卢曼的标识系统只是借用了层级结构的形式来形成超文本,实际上并不会以层级结构来组织笔记内容。随着笔记系统增长,层级标识符就会变得没那么重要。不过,你还是需要将笔记卡片放在容易找到的地方,因为查找临近的笔记要比查找放在另一个盒子里的笔记更方便。
基于时间的 ID。与基于纸张的卡片盒笔记系统不同的是,电子版的卡片盒笔记系统 中的笔记卡片不会存放在实体的地方,比如一个抽屉里。这种情况下,要创建超文本系统,你只需要一个地址。时间戳(time-stamp)是创建唯一标识符最简单的方法。比如基于时间的 ID 可以是:202012011838(你创建某条笔记时的时间:2020年12月1日,18点,38分)。
其他任意的唯一字符串。你可以用程序生成一个随机但是唯一的字符串,或者其他任何你想要的字符串。如果让我用这种方法,而不用基于时间的ID,原因可能是我想缩短 ID 的位数。比如,如果你将时间戳编码为十六进制的数,那么20205191402可以缩短为2F08729AEA。这种对时间戳直接翻译的方法使 ID 缩短了两位,你可能还能想到其他的办法。但是,这类办法可能会带来一些不必要的复杂性,也无法告诉你某条笔记的创建时间,因此我不建议使用这种方法。
你也可以使用 Zettel 的标题作为它的 ID。只要标题是唯一的,标题就可以起到 ID 的作用。因此,为了保证标题的唯一性,标题一旦确定,就不能更改,除非你改变曾引用过该笔记的其他所有笔记。有些软件可以帮你处理这个问题,但我不推荐这样做。我更倾向于采用独立于特定软件的方式。
笔记卡片的主体内容
笔记卡片的主体包含任何你想记录的知识,它可以是一个论点,一个概念,一个例子,或者任何类似的东西。
需要注意的是,你要用自己的话来写这部分内容。你可以完全摘抄一段别人的观点,但是要让卡片盒笔记系统为你工作,其中一个核心规则就是用你自己的话,而不是复制粘贴一些你认为有用或者有见地的东西。这样会迫使你创造创造一个属于你自己版本的知识,有助加深你对学习材料的理解,加强你对所写内容的记忆。只有当笔记的内容是你自己的,你的卡片盒笔记系统才将真正属于你。
译者注: 同样一段材料,深层次的分析会比浅层次的分析产生更复杂、更持久、更强烈的记忆痕迹。——Levels of Processing model - Wikipedia
笔记内容的长度与你想创建的超文本系统直接相关。你想创建一个书摘(excerpt)网络?一条笔记应该包含一段书摘。你想创建一个由各种想法(thought)组成的思想之网?那么一个笔记卡片应该刚好包含一个想法。笔记卡片是有自己地址的基础实体(base entity)。因此,笔记卡片的长度,是由你想要达到的目标决定的。由于人的思维是以单个想法为单位工作的,而你设计卡片盒笔记系统的目的是捕获你的想法及其想法间的联系。因此,我们建议每条笔记的内容限制为单个想法。只有这样,你的卡片盒笔记系统 能协助你思考,而不是仅仅协助你创作书摘。
在我们的论坛上,Nick 问我会选择什么样的内容输入到卡片盒笔记系统。虽然你可以在笔记里写任何东西,但是我建议输入知识而非信息(Knowledge instead of information)。
实际上,很容易区分知识和信息。信息大多数时候可以用一句话概括,而且大多数时候信息是“死”的。
比如,下面这条是信息:
我,Sascha,在 20 May 2020 09:14 时完成了文章的初稿,文章标题是”Zettelkasten - An introduction”.
这段文字对你有什么用呢?如果你是研究卡片盒笔记法的历史学家,你可以把它作为一篇介绍Z卡片盒笔记法展历史的特定事件,并追踪这个话题在互联网上的传播和发展,会为你的工作建立一个实证的信息基础。但是对于大多数人来说,这段话是无用的,因为它仅仅陈述了一个事实,是没有生命力的信息,不是知识。
因此,作为一条经验法则,你应该对信息进行处理并从中有所收获。你应该在笔记中添加一定的上下文内容,并与其他笔记建立连接,以便让信息转化为知识。即便你没有直接使用你创造的知识,只要你用笔记之间的连接丰富了你正在处理的信息,你就走在了正确的道路上。你不需要像 @grayen一样担心:
我有时会想研究一下是什么让网页中的内容值得我记笔记,我不知道是否该把它写进我的笔记卡片里,我不想为了记笔记而记笔记。我不知道这些东西对我来说是短时间内有用,还是长期有用。我有时不想仅仅为了处理我的某个想法或解决某个疑问就写一张笔记卡片 。我不想把我的 卡片盒笔记系统变成一份繁忙的工作,繁忙必定带来拖延。
如果你不清楚是否该写一条笔记,那你应该在截止日期内写这条笔记。如果你早已知道某条知识是否与你最终的产品相关,那就没有理由做笔记,因为你脑海里已经有了最终产品的形态。记住,你往 卡片盒笔记系统中添加的知识在未来都可能是有用的,只不过在你添加它们的时候你看不到罢了。
在实践中,你需要在广泛地做笔记和专注于你当前的项目之间做出妥协。你无法只是随便记下感兴趣的东西,并期望有所收获。你应该把你手头上正在进行的项目作为主线来指导你的工作,同时允许自己稍微偏离一点这条主线,偏离程度取决于你当前项目的截止日期。
这种方式的好处是,你能更多的保持写作的状态。我已经记不清在进行某个项目时为另外一个项目写了多少好的想法和文字了。通过保持你的写作状态,从长远看会产生更多有用的想法。
完成项目的过程会产生各种各样的副产品
这些副产品不是无用的,它们将成为你未来项目的宝贵知识。同时,它们与你的 卡片盒笔记系统的其他部分建立了联系,并将丰富你的学习经验。
实例:我有一个长期的写书计划。写一本非常全面的有关营养学的书籍。其中有一章专门讨论营养学与其他相关领域的融合:压力、训练和日常生活的组织等。我一定程度的放任自己的写作,结果就是我将这一章扩展成了一整本书。
其他例子
如果你正在写毕业论文,时间紧迫,那么你应该尽量少的偏离主线,并专注于论文相关的材料
如果你是位退休的机械工程师,在一个自由而美好的周末,打算用 卡片盒笔记系统来写一本小说,那么你想怎么偏离就怎么偏离,恣意地享受人生吧。
如果你是一名护士,想出版一本关于如何处理系统的小书。不要为了专注于工作而压力太大,你已经为别人付出很多,你应该享受写作的过程,满足你的好奇心,即使这拖慢你的进度。
如果你有雄心勃勃的A型人格,那么你可以尽可能地偏离主线。你的个性会确保你任何时候能很快地回到跟你项目相关的材料上来。
应该集中精力还是允许一些偏离,这与 卡片盒笔记系统Method 无关,更多的取决于你个人的情况。
参考文献
在 笔记卡片的底部是 MultiMarkdown 语法中的 citekeys 的,这条参考文献在主体内容的最后一行被引用了一次
参考文献一般位于在笔记卡片的底部,用来说明信息的来源。一般为 卡片盒笔记系统外部的参考文献,比如书籍、论文或者网络上的文章。
你可以使用文献管理软件来管理这些参考文献,比如 BibDesk。它将包含书目数据,并提供 citekeys。Citekeys 类似于 ID,你可以通过它们指向你引用用的参考文献((citekey的一种常见格式是[lastnameYEAR])
有时候,其他的笔记卡片可能是你灵感的来源。在这种情况下,你曾经记的笔记或者处理过的信息成为了你当下思考的基础。你引用笔记卡片时,通过 ID 连接到其他笔记卡片,将新旧笔记联系起来。
如果你没有引用任何参考文献,也许意味着你在知识的森林里漫步,突然洞察了世界的本质。这种情况下,你无需在参考文献部分做任何事情,因为这是你的原创想法。
一个完整的卡片笔记
下面这张图标注了前面提到的所有元素。
一个完整的笔记卡片
目前为止,我们只创建了单个的 Zettel。现在,我们来看看 Zettel 之间的联系。
连接笔记卡片
左侧的 笔记卡片通过 ID 与右侧的 笔记卡片连接
与普通的笔记系统相比,卡片盒笔记系统真正的魔力来自于对连接(connection)的高度重视。每一个新的笔记卡片都需要与其他的笔记卡片建立某种关系。卢曼曾这样强调连接进每一个新 Zettel 的重要性:
如果解决某个问题的方式有好几种,我们只需通过一个链接将这些可能性联系起来。通常,笔记中的一条笔记中有多个链接与好几个其他笔记相连。在这种情况下,我们不但要建立正向的链接,还应该在被链接的笔记中建立反向链接(backlinks)。在这个过程中,我们会发现所写的笔记内容也会越来越丰富。—— Communicating with Slip Boxes by Niklas Luhmann
在笔记间建立连接的最主要的好处是对你大脑的影响。当你把很多知识碎片连接起来时,你会在知识之间建立联系。这不但有助于记忆知识,还能训练大脑看到知识的模式(patterns)。
比方说,你读一篇关于蚁穴的文章时,突然想到:“蚁穴内的结构很像我们的工厂!”也就是说,你发现了工厂和蚁穴之间的联系。你为什么能看到这些联系呢?因为可以用同一种典型的模式描述蚁穴和工厂。在这些联系中可能包含一些你之前不知道的东西。你可能会想,蚁穴中会不会有一些特别有效的路径,能被用在工厂的布局中,以便让工厂更高效的运转?
当你连接笔记时,你会学习和领会新东西,并通过两个方面来拓展自己的认知:(a)你的知识会增加,(b) 你会成为一个更好的观察者(observer)。通过成为更好的观察者,你能从观察中获得多感悟(insights)。你会意识到,现实生活中许多看似无关的事物都遵从相同的模式,并且发现这些模式的难度对你来说会越来越低。
为了能最大限度地发挥连接的作用,你一定要明确说明为什么要建立连接,即建立连接的原因。一般情况下,原因通过链接的上下文来体现,比如:
Investing for normal people aims towards positiv chashflow Cashflow Liquidity Investing starts with liquidity:[[202001121202]] You have to have the liquidity to make investment decisions unless you are using other people’s money (e.g. using leverage). But if you have to start with liquidity what is the end of investing? If Cash is the Alpha, Cashflow is the Omega. If you buy real estate, there is no question that positive cashflow is key to a good investment. The same is true for stocks. Why should we expose ourselves to the uncertainty of the market if we want to support our normal lifestyles.As an ordinary person, you don’t speculate with real estate, but expect it to generate a steady cashflow: You rent it out. There is no reason we shouldn’t apply this principle to stocks. Dividend should be necessary. However, this line of thought should be viewed in the light of the knockout method of investing: [[202001171046]]
两个链接的上下文
在第一段文字中,这条笔记卡片连接到了的一个链接地址为 [[202001121202]] 的笔记,这条笔记的内容解释了为什么投资应该优先考虑流动性。但是,第一段文字本身也给这个链接添加了一些上下文,用来解释重视流动性的原因。这段上下文是用来告诉以后再看这条笔记的自己:如果你沿着这个链接去浏览笔记,你能得到什么。
以这种方式建立的连接是 卡片盒笔记系统创造知识的机制之一:明确地说明连接的原因。
如果你只添加链接而不做任何解释,那么你将无法创造知识。未来的的你可能根本不知道为什么要沿着链接去浏览笔记。试想一下,你创造了一张由各种想法组成的思想之网,但是你却无法确定沿着某个链接是否可以把你引向有意义的东西,那么在笔记间冲浪时肯定会给你一种失望的感觉。未来的你甚至会吐槽过去的自己不靠谱。
总之,建立连接却不指明原因的习惯是无法生产知识的。而且这种习惯会让你的工作变得随意和浅薄,降低你作为知识型工作者的生产力。
结构化笔记(Structure Note)
一个 卡片盒笔记系统不应该只有通过笔记卡片和它们的联系由下而上建立的网状笔记,一些层次结构(hierarchical structure)也是非常有用的,卢曼自己也引入了类似的结构。
我们可以看看卢曼是如何使用笔记索引表的(register)。在索引表上,并不是每个关键词后面都会列出所有与其相关的笔记卡片的 ID。只有最中心最重要的笔记可以成为某个主题的入口。
除了索引表,卢曼还有另外一种非常重要的笔记:枢纽笔记(hub notes)。枢纽笔记中列出了许多其他同属于某个主题的其他笔记。搜索相关联的笔记是卢曼式的 Zettelkasten(基于纸张的 Zettelkasten)面临的挑战,尤其是与电子卡片盒笔记系统相比。
层次结构的主要好处是增加创造知识的潜力,因为将知识结构化可以让你获得更多信息,是创造知识的有效手段。让我们一起探讨一下这种为 卡片盒笔记系统添加层级结构的结构化笔记。
一个结构化的笔记,具有目录结构以及相对应的思维导图形式
结构化笔记是元笔记(Meta-Note):它是一个关于其他笔记及其联系的笔记。卢曼的枢纽笔记作为快速通道,可在笔记网络中导航。结构化笔记的作用也是如此。比如,上图是一个关于卡片盒笔记法的结构化笔记。它类似于一个目录(table of contents),专门列出了所有有关这个主题的笔记。每当我写一个关于卡片盒笔记法的新笔记时,我都会将它的 ID 添加到这个结构化笔记上。
有关通用模型的结构化笔记
上图是我的另一个结构化笔记。这条笔记连接的每条笔记卡片都是心理模型(mental models)中的一种独立模型。举两个例子来说明:
Obstacle Model:从一种状态转变为另一种状态需要增加能量输出的观点。比如饥饿时,你需要增加能量输出(狩猎或采集),才能从饥饿状态变为饱食状态。又比如,你需要保持专注,增加精神能量的的输出才能完成一些之前未完成的任务。
Chemistry Mode:认为分子由原子组成,并假设原子不可被分隔的观点。卡片盒笔记法应用了该模型,认为每条笔记应该是原子化的。另外,Portal关于掌握技能的教学方法也应用了该模型。
记录 Chemistry Mode 的笔记被放置在通用模型的结构化笔记中的同时,也被放置在关于 卡片盒笔记法 方法的结构化笔记中。这种重叠交错的笔记(overlaps in Zettels)最终形成了一个半格结构(Semilattice structure):
与严格的树型结构不同,半格结构可以有从一个分支到另一个分支的交叉连接
实际上,即使是半格结构也无法完全反映 卡片盒笔记系统的真实情况。准确描绘 卡片盒笔记系统涉及到异构模型(Heterarchy)这个概念,但这已经超出本篇文章的范围了。
另外,结构化笔记不限于上面提到的那种嵌套的层次结构(hierarchical structure),也可以是非嵌套的顺序结构(sequential structure)。试想一下这种论证过程:因为 a→b→c,所以 a→c。结构化笔记也可以是这种结构,在这个论证过程的每一步都连接到一个相关的笔记卡片。比如:
(a) 刺激皮肤感知低温的感受器是 (b) 让人体适应寒冷的主要方法。 [[202005201056]] 而洗冷水澡可以充分刺激皮肤表面的低温感受器 [[202005201057]]。因此,(c) 洗冷水澡是锻炼人体适应寒冷可行方法。[[202005201058]]
通过 [[ID]] 连接到其他相关笔记卡片,给这个论证过程提供可靠的证据。而论证的逻辑结构就是这条结构化笔记的结构。
总结:关于其他笔记之间关系的笔记称为结构化笔记(Structure Note)。创建结构化笔记能够训练你发现通用知识模式的能力,能够从 卡片盒笔记系统发现结构化的有用信息供你未来使用。
如何实践卡片盒笔记法
软件选择
如果你选择使用软件来建立 Zettelkasten,那么这个软件应该具备以下功能:
能够创建超文本。对于不太懂技术的人,你可以使用软件提供的链接功能,直接跳转到被当前笔记连接的笔记,但是你也可以通过全文搜索来模拟直接跳转。随着时间推移和跨应用使用,你会发现基于搜索的方法更可靠更强大,但是也比较笨拙。你可以看看我用 TextMate 如何处理链接的。请记住,我没有为了实践 卡片盒笔记系统而修改 TextMate 的任何功能。
笔记之间的导航依靠两点:全文搜索和跟随链接。全文搜索相当于在卢曼的 卡片盒笔记系统中的利用索引表寻找入口(全文搜索显然更强大)。链接就是你了解的那个链接,前面已经解释了很多了。全文搜索是电子版的 卡片盒笔记系统的优势,基于纸张的 卡片盒笔记系统无法实现这个功能。
沙盒功能(sandbox)。卢曼的 卡片盒笔记系统的一个隐含部分是他的办公桌。他可以随便拿出一些笔记卡片,按照自己喜好把他们摆放在桌面上。电子版的 卡片盒笔记系统不容易做到这一点。在 The Archive(一款软件)中,我用结构化笔记模拟桌面。结构化笔记的排列方式能够体现笔记之间的层级,虽然不像桌面那么自由,但是也足够我使用了。
使用基于纸张的卡片盒笔记系统
如果你使用基于实体纸张来建立 Zettelkasten,那么只需按照卢曼的与滑盒交流一文中阐述的方法去做。
我以这种方式开始了我的 卡片盒笔记系统之旅,并实践了几个月。后来我转向了电子版的 Zettelkasten。转移笔记的过程非常痛苦,除非你笔记上的字迹非常工整清晰,并且可以使用 OCR 方法识别,否则没有什么自动化的方法。
The Archive
The Archive 这款软件的主要理念是坚持软件不可知论(software-agnostic)和以纯文本(plain text)的形式记录笔记。
软件不可知论希望用户完全掌握自己的数据。例如,有的软件将笔记存储在一种封闭的文件格式中,其他软件无法读取。有的软件会提高你换用其他软件的门槛,比如让数据的导出过程变得很困难,或者训练用户依赖一些其他软件没有的功能。我们试图避免这种情况,因此我们决定让搜索负责执行实践 卡片盒笔记系统需要的功能。在 The Archive 中,从一条笔记跳转到另一条被链接的笔记的功能也是通过搜索实现的。全文搜索在计算机上无处不在,因此你可以用世界上几乎所有的纯文本编辑器重现你的 卡片盒笔记系统工作流程。
纯文本是最通用也最耐用的文件格式, The Archive 使用纯文本作为主要的存储格式。
总之,The Archive 管理着一个纯文本文件构成的文件见,你可以在地址栏进行搜索来访问你的文件。
在 The Archive 中,笔记是这样的:
简短的,具有一定格式的笔记卡片
Zettel 的 ID既在文件名中,也在文件内容中。这样做是为了添加一些冗余信息,以便以后在其他软件上访问你的 Zettelkasten。比如,我常在其他电脑上使用 Dropbox 访问我的笔记,而 Dropbox 只允许使用文件名搜索文件。所以,我在文件名中加入 ID,以便能够通过 Dropbox 的搜索功能手动地跟随笔记间的链接;
ID 是基于时间的。它永远不会改变,所以你可以在不破坏任何链接的情况下随意更改标题;
如果你想跟随一个链接,你可以搜索双方括号内的 ID。因此,你可以使用任何具有搜索功能的软件来跟随笔记里的链接。在 The Archive 中,将 ID 放在双方括号里就可以创造一条超链接;
标签以#开头。如果你点击标签,The Archive 将搜索它。标签的作用是对笔记分组;
The Archive 使用 Markdown 来标记文本。Markdown 可读性很强,同时广泛应用于其他软件。许多文本编辑器都支持 Markdown,你可以用它们生成漂亮的 PDF 文件,也可以用来写文章或者做 Presentation;
基于扩展的 MultiMarkdown 语法做引文和书目的参考文献。它能与 BibTeX 完美配合,可以将书目数据保存在纯文本文件中。我们推荐在 Mac 下使用开源工具 BibDesk 和 Windows 下 使用 JabRef 来管理引文数据,并通过生成的 citekeys 将其连接到 Zettelkasten
本文所有截图都来自 The Archive。
Zettel 的 ID既在文件名中,也在文件内容中。这样做是为了添加一些冗余信息,以便以后在其他软件上访问你的 Zettelkasten。比如,我常在其他电脑上使用 Dropbox 访问我的笔记,而 Dropbox 只允许使用文件名搜索文件。所以,我在文件名中加入 ID,以便能够通过 Dropbox 的搜索功能手动地跟随笔记间的链接;
ID 是基于时间的。它永远不会改变,所以你可以在不破坏任何链接的情况下随意更改标题;
如果你想跟随一个链接,你可以搜索双方括号内的 ID。因此,你可以使用任何具有搜索功能的软件来跟随笔记里的链接。在 The Archive 中,将 ID 放在双方括号里就可以创造一条超链接;
标签以#开头。如果你点击标签,The Archive 将搜索它。标签的作用是对笔记分组;
The Archive 使用 Markdown 来标记文本。Markdown 可读性很强,同时广泛应用于其他软件。许多文本编辑器都支持 Markdown,你可以用它们生成漂亮的 PDF 文件,也可以用来写文章或者做 Presentation;
基于扩展的 MultiMarkdown 语法做引文和书目的参考文献。它能与 BibTeX 完美配合,可以将书目数据保存在纯文本文件中。我们推荐在 Mac 下使用开源工具 BibDesk 和 Windows 下 使用 JabRef 来管理引文数据,并通过生成的 citekeys 将其连接到 Zettelkasten
如果你选择使用软件来建立 Zettelkasten,那么这个软件应该具备以下功能:
能够创建超文本。对于不太懂技术的人,你可以使用软件提供的链接功能,直接跳转到被当前笔记连接的笔记,但是你也可以通过全文搜索来模拟直接跳转。随着时间推移和跨应用使用,你会发现基于搜索的方法更可靠更强大,但是也比较笨拙。你可以看看我用 TextMate 如何处理链接的。请记住,我没有为了实践 卡片盒笔记系统而修改 TextMate 的任何功能。
笔记之间的导航依靠两点:全文搜索和跟随链接。全文搜索相当于在卢曼的 卡片盒笔记系统中的利用索引表寻找入口(全文搜索显然更强大)。链接就是你了解的那个链接,前面已经解释了很多了。全文搜索是电子版的 卡片盒笔记系统的优势,基于纸张的 卡片盒笔记系统无法实现这个功能。
沙盒功能(sandbox)。卢曼的 卡片盒笔记系统的一个隐含部分是他的办公桌。他可以随便拿出一些笔记卡片,按照自己喜好把他们摆放在桌面上。电子版的 卡片盒笔记系统不容易做到这一点。在 The Archive(一款软件)中,我用结构化笔记模拟桌面。结构化笔记的排列方式能够体现笔记之间的层级,虽然不像桌面那么自由,但是也足够我使用了。
结构化笔记(Structure Note)
我们可以看看卢曼是如何使用笔记索引表的(register)。在索引表上,并不是每个关键词后面都会列出所有与其相关的笔记卡片的 ID。只有最中心最重要的笔记可以成为某个主题的入口。
除了索引表,卢曼还有另外一种非常重要的笔记:枢纽笔记(hub notes)。枢纽笔记中列出了许多其他同属于某个主题的其他笔记。搜索相关联的笔记是卢曼式的 Zettelkasten(基于纸张的 Zettelkasten)面临的挑战,尤其是与电子卡片盒笔记系统相比。
卡片盒笔记系统最基本的原则是:使用 ID 作为地址链接,建立超文本。
软件不可知论(software-agnostic)
软件不可知论希望用户完全掌握自己的数据。例如,有的软件将笔记存储在一种封闭的文件格式中,其他软件无法读取。有的软件会提高你换用其他软件的门槛,比如让数据的导出过程变得很困难,或者训练用户依赖一些其他软件没有的功能。
纯文本(plain text)
纯文本是最通用也最耐用的文件格式
感悟(insights)
为了能最大限度地发挥连接的作用,你一定要明确说明为什么要建立连接,即建立连接的原因。一般情况下,原因通过链接的上下文来体现,比如:
如果你只添加链接而不做任何解释,那么你将无法创造知识。未来的的你可能根本不知道为什么要沿着链接去浏览笔记。试想一下,你创造了一张由各种想法组成的思想之网,但是你却无法确定沿着某个链接是否可以把你引向有意义的东西,那么在笔记间冲浪时肯定会给你一种失望的感觉。未来的你甚至会吐槽过去的自己不靠谱。
参考文献一般位于在笔记卡片的底部,用来说明信息的来源。一般为 卡片盒笔记系统外部的参考文献,比如书籍、论文或者网络上的文章。
你可以使用文献管理软件来管理这些参考文献,比如 BibDesk。它将包含书目数据,并提供 citekeys。Citekeys 类似于 ID,你可以通过它们指向你引用用的参考文献((citekey的一种常见格式是[lastnameYEAR])
如果你没有引用任何参考文献,也许意味着你在知识的森林里漫步,突然洞察了世界的本质。这种情况下,你无需在参考文献部分做任何事情,因为这是你的原创想法。
笔记卡片的主体内容
笔记卡片的主体包含任何你想记录的知识,它可以是一个论点,一个概念,一个例子,或者任何类似的东西。
需要注意的是,你要用自己的话来写这部分内容。你可以完全摘抄一段别人的观点,但是要让卡片盒笔记系统为你工作,其中一个核心规则就是用你自己的话,而不是复制粘贴一些你认为有用或者有见地的东西。这样会迫使你创造创造一个属于你自己版本的知识,有助加深你对学习材料的理解,加强你对所写内容的记忆。只有当笔记的内容是你自己的,你的卡片盒笔记系统才将真正属于你。
译者注: 同样一段材料,深层次的分析会比浅层次的分析产生更复杂、更持久、更强烈的记忆痕迹。——Levels of Processing model - Wikipedia
笔记内容的长度与你想创建的超文本系统直接相关。你想创建一个书摘(excerpt)网络?一条笔记应该包含一段书摘。你想创建一个由各种想法(thought)组成的思想之网?那么一个笔记卡片应该刚好包含一个想法。笔记卡片是有自己地址的基础实体(base entity)。因此,笔记卡片的长度,是由你想要达到的目标决定的。由于人的思维是以单个想法为单位工作的,而你设计卡片盒笔记系统的目的是捕获你的想法及其想法间的联系。因此,我们建议每条笔记的内容限制为单个想法。只有这样,你的卡片盒笔记系统 能协助你思考,而不是仅仅协助你创作书摘。
在我们的论坛上,Nick 问我会选择什么样的内容输入到卡片盒笔记系统。虽然你可以在笔记里写任何东西,但是我建议输入知识而非信息(Knowledge instead of information)。
实际上,很容易区分知识和信息。信息大多数时候可以用一句话概括,而且大多数时候信息是“死”的。
比如,下面这条是信息:
我,Sascha,在 20 May 2020 09:14 时完成了文章的初稿,文章标题是”Zettelkasten - An introduction”.
这段文字对你有什么用呢?如果你是研究卡片盒笔记法的历史学家,你可以把它作为一篇介绍Z卡片盒笔记法展历史的特定事件,并追踪这个话题在互联网上的传播和发展,会为你的工作建立一个实证的信息基础。但是对于大多数人来说,这段话是无用的,因为它仅仅陈述了一个事实,是没有生命力的信息,不是知识。
因此,作为一条经验法则,你应该对信息进行处理并从中有所收获。你应该在笔记中添加一定的上下文内容,并与其他笔记建立连接,以便让信息转化为知识。即便你没有直接使用你创造的知识,只要你用笔记之间的连接丰富了你正在处理的信息,你就走在了正确的道路上。你不需要像 @grayen一样担心:
我有时会想研究一下是什么让网页中的内容值得我记笔记,我不知道是否该把它写进我的笔记卡片里,我不想为了记笔记而记笔记。我不知道这些东西对我来说是短时间内有用,还是长期有用。我有时不想仅仅为了处理我的某个想法或解决某个疑问就写一张笔记卡片 。我不想把我的 卡片盒笔记系统变成一份繁忙的工作,繁忙必定带来拖延。
如果你不清楚是否该写一条笔记,那你应该在截止日期内写这条笔记。如果你早已知道某条知识是否与你最终的产品相关,那就没有理由做笔记,因为你脑海里已经有了最终产品的形态。记住,你往 卡片盒笔记系统中添加的知识在未来都可能是有用的,只不过在你添加它们的时候你看不到罢了。
在实践中,你需要在广泛地做笔记和专注于你当前的项目之间做出妥协。你无法只是随便记下感兴趣的东西,并期望有所收获。你应该把你手头上正在进行的项目作为主线来指导你的工作,同时允许自己稍微偏离一点这条主线,偏离程度取决于你当前项目的截止日期。
这种方式的好处是,你能更多的保持写作的状态。我已经记不清在进行某个项目时为另外一个项目写了多少好的想法和文字了。通过保持你的写作状态,从长远看会产生更多有用的想法。
完成项目的过程会产生各种各样的副产品
这些副产品不是无用的,它们将成为你未来项目的宝贵知识。同时,它们与你的 卡片盒笔记系统的其他部分建立了联系,并将丰富你的学习经验。
实例:我有一个长期的写书计划。写一本非常全面的有关营养学的书籍。其中有一章专门讨论营养学与其他相关领域的融合:压力、训练和日常生活的组织等。我一定程度的放任自己的写作,结果就是我将这一章扩展成了一整本书。
其他例子
如果你正在写毕业论文,时间紧迫,那么你应该尽量少的偏离主线,并专注于论文相关的材料
如果你是位退休的机械工程师,在一个自由而美好的周末,打算用 卡片盒笔记系统来写一本小说,那么你想怎么偏离就怎么偏离,恣意地享受人生吧。
如果你是一名护士,想出版一本关于如何处理系统的小书。不要为了专注于工作而压力太大,你已经为别人付出很多,你应该享受写作的过程,满足你的好奇心,即使这拖慢你的进度。
如果你有雄心勃勃的A型人格,那么你可以尽可能地偏离主线。你的个性会确保你任何时候能很快地回到跟你项目相关的材料上来。
应该集中精力还是允许一些偏离,这与 卡片盒笔记系统Method 无关,更多的取决于你个人的情况。
唯一标识符
唯一标识符(ID)是创建卡片盒笔记系统的必备条件,只有拥有了唯一标识符,才可以定位到单张笔记卡片。只有拥有这种能力,才可以创建一张能在知识型工作中协助你的思想之网。
通常,给笔记添加唯一标识符的方法有以下几种:
卢曼式的 ID。前面提到,卢曼给每张笔记卡片一个由数字和字母组成的标识符,这种标识符利用层级结构的形式给笔记卡片编号。如果你准备使用纸质的
Zettelkasten,我建议你使用这种方法,因为它能更好的组织你的笔记。下面讲的其他类型的标识符,很难在纸质版的卡片盒系统中取得好效果。需要注意,卢曼的标识系统只是借用了层级结构的形式来形成超文本,实际上并不会以层级结构来组织笔记内容。随着笔记系统增长,层级标识符就会变得没那么重要。不过,你还是需要将笔记卡片放在容易找到的地方,因为查找临近的笔记要比查找放在另一个盒子里的笔记更方便。
基于时间的 ID。与基于纸张的卡片盒笔记系统不同的是,电子版的卡片盒笔记系统 中的笔记卡片不会存放在实体的地方,比如一个抽屉里。这种情况下,要创建超文本系统,你只需要一个地址。时间戳(time-stamp)是创建唯一标识符最简单的方法。比如基于时间的 ID 可以是:202012011838(你创建某条笔记时的时间:2020年12月1日,18点,38分)。
其他任意的唯一字符串。你可以用程序生成一个随机但是唯一的字符串,或者其他任何你想要的字符串。如果让我用这种方法,而不用基于时间的ID,原因可能是我想缩短 ID 的位数。比如,如果你将时间戳编码为十六进制的数,那么20205191402可以缩短为2F08729AEA。这种对时间戳直接翻译的方法使 ID 缩短了两位,你可能还能想到其他的办法。但是,这类办法可能会带来一些不必要的复杂性,也无法告诉你某条笔记的创建时间,因此我不建议使用这种方法。
你也可以使用 Zettel 的标题作为它的 ID。只要标题是唯一的,标题就可以起到 ID 的作用。因此,为了保证标题的唯一性,标题一旦确定,就不能更改,除非你改变曾引用过该笔记的其他所有笔记。有些软件可以帮你处理这个问题,但我不推荐这样做。我更倾向于采用独立于特定软件的方式。
单个笔记或者说 Zettel 是什么样的呢?每条笔记由三部分组成:
唯一标识符:它为你的笔记提供一个明确的地址;
笔记的正文:这是你记录的笔记内容,一般为一段简短的原子化的信息;
参考文献:如果你的内容来源于外部,你可以在每条笔记的底部写上信息来源,如果你记录的是你自己的想法,则留空。
其实就是这么简单,如果你不清楚有没有做对,只需要回头看看这个基础知识。要形成超文本系统,最起码需要给每条笔记一个地址,也就是一个唯一的标识符,当然还有你记录的笔记内容。
kok的笔记本
关于我
Pegboard
周刊
播客
分类
系列文章
搜索
闲棋冷子-2-2021年我读过哪些很棒的书
二月 28, 2022
· 3 分钟 · kokdemo
目录
大家好,这篇 Newsletter 内容比较简单,就是推荐一些我和朋友们在21年看过觉得“很棒”的书。
朋友们的推荐
首先是朋友们推荐的几本:
• 《大国大城》
• 《正念-专注内心思考的艺术》:来自DK出版社
• 《中国是部金融史》
• 《后物欲时代的来临》
• 《第一律法》
• 《凤凰架构》:一本从架构视角讲解构建大型分布式系统的书。
还有几本书,朋友们认真写了推荐理由,我贴在这里:
• 《刘擎西方现代思想讲义》:“人类因为理性而伟大,因为指导理性的局限而成熟”
• 《哈利波特全集》:“成年人也容易读进去,也能从中找到所需要的力量,远远不止儿童读物”
• 《饱食穷民》:“可以看看当年的日本社会和现在有哪些相似之处”
推荐的这些书中,我只看过《大国大城》(哈利波特没看完),这本书讲了不少中国城市化过程中遇到的问题,公共资源的困境,越来越严重的城市病等等。
这本书非常值得一看,书本身写的也很好读,看完当时激起了我的一些兴趣,陆陆续续还看了《美国大城市的死与生》《芝加哥规划》等。
朋友推荐的书里面我对《现代思想讲义》、《正念》这两本书更感兴趣一些,已经加了“想读”标记。
我的推荐
以下是我21年读过觉得很棒的书:
• 《蛤蟆先生去看心理医生》:这本书薄薄一本,很快就能看完,也不是说教气息很浓的心理书籍,而是简单的讲了几个小故事,让我们感受到蛤蟆先生如何脱离困境,重拾生活的乐趣。
• 《潜规则》:这本书来自B站UP主半佛仙人的推荐,我也找时间看了一下,属于文集类的作品,一共19篇文章。吴思先生发明了潜规则这个词,并且给了它非常完备的解释。美中不足的一点就是这本书部分文章中间存在少量的重复。
• 《卡片笔记写作法》:读这本书主要是因为flomo创始人少楠的推荐,最早flomo开始测试的时候,我当时读过flomo思想出处的《如何有效整理信息》,还写了一篇文章来聊这件事。这本书相对来说更接地气,也更加适合实践。但同样的,如果你对卡片式写作和知识管理并没有了解和实践,在读起来的时候会缺少不少沉浸感。
• 《麦肯基疗法》:这本书里面教的方法确实能有效缓解颈部和腰部的疼痛,非常实用。还有本类似的书,叫做《腰背维修师》,我已经买了放在书架上了,最近准备看看。
• 《不拘一格》:一本关于网飞企业文化的一本书,就看人家公司近乎理想主义的公司文化,我只能说很羡慕。这本书我还写过更详细的分享,大家可以点击这篇文章查看.
• 《分析与思考》:这本书的视角非常高,从总体的角度来讲政府的财政、货币房地产政策,而且全都是大白话,也很容易看明白。还有另一本书可以放在一起读,叫做《置身事内》,这本书则是从学者的角度来讲国家的财政政策和发展。
闲棋冷子-1-相信你的身体,它不会骗你
二月 24, 2022
· 5 分钟 · kokdemo
目录
这周比较让人伤感的新闻之一,是某知名互联网公司的一名高级工程师在公司附近健身时,永远的离开了这个世界。虽然他的死因目前还没有公布,但恐怕与较高的工作强度分不开关系。
这一事件与之前B站审核员“暮色木心”,多多买菜派驻新疆的员工比较相似,都是由于较高的工作强度压垮了身体,导致了猝死的出现。
你的身体不会骗人,它累了就是累了。
你的身体到底是什么状态
考虑到能看到这篇文章的朋友与我的工作环境相仿,我描述以下的情况,大家来看看自己是否存在:
体重过重(BMI>24)
每周的中等运动和强度运动时间少于60分钟
存在颈部、腰部不适
摄入油、盐的水平较高,超过建议摄入值
睡眠不足8小时
当然大家如果想更详细的了解一下,可以访问这个亚健康自测量表来测试一下:https://m.medsci.cn/scale/show.do?id=9466225369
你的身体素质比你想象中还差,你没身体资本去“拼”。
人体的身体素质在青春期逐步提升,在20~40岁缓慢下滑,在40岁后快速下滑,这是不可逆的自然规律。
如果你小的时候身体底子本来就不好(还记得被占用的体育课吗),年轻的时候也不注意,那猝死风险将会越来越大。
既然工作很辛苦,锻炼有用吗?
大家可能会产生这样的念头,其实已经前进了一大步了。据我了解,在上述新闻中的某互联网大厂附近就有很多配套的健身房,且主动去健身的人相当多。
但你的身体的机能恢复(锻炼),其实是由三个部分构成的,也就是:
破坏:通过超越之前强度的抗阻力训练(或有氧训练),来强迫自己的身体开始调整到新的状态,破坏了己有的稳态。
营养:摄入优质的营养(蛋白质、维生素、糖分)来帮助肌肉组织增长肥大,帮助新陈代谢加速。
恢复:通过足量的恢复来将营养吸收到自己的身体中,身体就会慢慢调整到一个更好的稳态。
这三个部分是循环递进的,不能少掉任何一环。大家如果有看过一些专业健体健美运动员的讲解或视频,你会发现他们也或多或少会提到【恢复】的过程,譬如这个UP主“短腿小萝卜”的视频。
如果真要有个优先级的话,足量的恢复和睡眠可能是这三个环节中最重要的。
但是很可惜,在这些案例中,大部分人下班的时间都极其晚,已经占用了一部分宝贵的睡眠时间。加上隔三差五还熬夜通宵,简直就是在自己脆弱的身体上狠狠踹了几脚。
这种情况下,就别加班锻炼了,直接回家休息躺着吧。
关于猝死的扩展阅读,见:
https://mp.weixin.qq.com/s/bn4…
学习《工伤保险条例》,但希望用不上
如果、万一,真的出现了这种情况,建议大家认真的学习《工伤保险条例》,里面关于工伤认定的部分,我摘录出来:
第十四条 职工有下列情形之一的,应当认定为工伤:
(一)在工作时间和工作场所内,因工作原因受到事故伤害的;
(二)工作时间前后在工作场所内,从事与工作有关的预备性或者收尾性工作受到事故伤害的;
(三)在工作时间和工作场所内,因履行工作职责受到暴力等意外伤害的;
(四)患职业病的;
(五)因工外出期间,由于工作原因受到伤害或者发生事故下落不明的;
(六)在上下班途中,受到非本人主要责任的交通事故或者城市轨道交通、客运轮渡、火车事故伤害的;
(七)法律、行政法规规定应当认定为工伤的其他情形。
第十五条 职工有下列情形之一的,视同工伤:
(一)在工作时间和工作岗位,突发疾病死亡或者在48小时之内经抢救无效死亡的;
(二)在抢险救灾等维护国家利益、公共利益活动中受到伤害的;
(三)职工原在军队服役,因战、因公负伤致残,已取得革命伤残军人证,到用人单位后旧伤复发的。
希望有关部门可以给予他一个公允的工伤认定吧。
利用 Python 批量合并 Excel 文件
一月 17, 2020
· 2 分钟 · kokdemo
目录
我经常需要手工处理一些日报,怎么提升工作的效率呢?
用 Python !
背景是这样的:
每天都会收到一个项目的数据日报,然后我需要将这些日报和之前的历史数据进行合并。更复杂的情况是,每个日报都不止一个sheet,如果手工复制的话,一个文件就需要复制三遍。
想想工作量还是很大的,所以就需要通过python来解决这个问题了。
打开 Anaconda,点击 JupyterLab,启动。(是的我越来越懒了,懒到不愿意去维护开发环境了)
一、导入库
这里会使用 os ,xlrd,pandas,pathlib 四个库。
xlrd 是用来读取 xls 文件的,pandas 是用来合并数据和写文件的。
pathlib 是用来兼容windows和unix平台(linux or macos)的文件路径的
import os
import xlrd
import pandas as pd
from pathlib import Path
二、遍历要合并的文件
给一个目标文件夹,将这里面全部的文件都生成文件路径。
def file_name(file_dir):
list=[]
for file in os.listdir(file_dir):
if os.path.splitext(file)[1] == ‘.xls’ or os.path.splitext(file)[1] == ‘.xlsx’:
list.append(file)
return list
三、合并输出文件
def merge_xlsx(path,filenames,sheet_num,output_filename):
data = [] 定义一个空list
title = []
path_folder = Path(path)
for i in range(len(filenames)):
read_xlsx = xlrd.open_workbook(path_folder / filenames[i])
sheet_num_data = read_xlsx.sheets()[sheet_num] 查看指定sheet_num的数据
title = sheet_num_data.row_values(0) 表头
for j in range(1,sheet_num_data.nrows): 逐行打印
data.append(sheet_num_data.row_values(j))
content= pd.DataFrame(data)
修改表头
content.columns = title
写入excel文件
output_path = path_folder / ‘output’
output_filename_xlsx = output_filename + ‘.xlsx’
if not os.path.exists(output_path):
print(“output folder not exist, create it”)
os.mkdir(output_path)
content.to_excel((output_path / output_filename_xlsx), header=True, index=False)
print(“merge success”)
四、写入变量并运行
下面是完整的代码:
import os
import xlrd
import pandas as pd
from pathlib import Path
def get_filename(file_dir):
list=[]
for file in os.listdir(file_dir):
if os.path.splitext(file)[1] == ‘.xls’ or os.path.splitext(file)[1] == ‘.xlsx’:
list.append(file)
return list
def merge_xlsx(path,filenames,sheet_num,output_filename):
data = [] 定义一个空list
title = []
path_folder = Path(path)
for i in range(len(filenames)):
read_xlsx = xlrd.open_workbook(path_folder / filenames[i])
sheet_num_data = read_xlsx.sheets()[sheet_num] 查看指定sheet_num的数据
title = sheet_num_data.row_values(0) 表头
for j in range(1,sheet_num_data.nrows): 逐行打印
data.append(sheet_num_data.row_values(j))
content= pd.DataFrame(data)
修改表头
content.columns = title
写入excel文件
output_path = path_folder / ‘output’
output_filename_xlsx = output_filename + ‘.xlsx’
if not os.path.exists(output_path):
print(“output folder not exist, create it”)
os.mkdir(output_path)
content.to_excel((output_path / output_filename_xlsx), header=True, index=False)
print(“merge success”)
if name == “main”:
path = r’/Your/Path’ 这里无论什么平台都按照unix风格来输入文件路径
filenames = get_filename(path)
merge_xlsx(path,filenames,0,“sheet1”) 合并文件中第一个表的数据,输出到 output/sheet1.xlsx中
merge_xlsx(path,filenames,1,“sheet1”) 合并文件中第二个表的数据,输出到 output/sheet2.xlsx中
使用了之后,原来合并一周日报要半个小时,现在只用运行一遍,效率高太多啦!
« 上一页
选小米10 还是小米10 Pro?
下一页 »
为什么家国梦不好玩?
昵称
邮箱
网址
欢迎评论
0 字
评论
按正序按倒序按热度
Powered by Waline v2.15.8
© 2023 kok的笔记本 Powered by Hugo & PaperMod
kok的笔记本
关于我
Pegboard
周刊
播客
分类
系列文章
搜索
找到并和正确使用无版权资源
八月 20, 2019
· 4 分钟 · kokdemo
目录
在工作中,难免需要找到一些素材,如何正确的使用它们,是一个要注意的问题。
本文截止目前已经收录了:
图片:7个
视频:6个
特效:1个
音乐:8个
音效:2个
插画:2个
icon:8个
字体:13个
什么是版权?
版权也叫著作权,是指表述创作者对于其作品复制权利的所有权。未经许可,不得擅自复制,使用或扭曲他人的著作。
前一段时间,视觉中国的事情炒的沸沸扬扬,视觉中国就是一家代理图片版权的公司,他们和全球好几家版权公司达成了合作关系,所以你在网上随便找一张图很容易陷入侵权的窘境。
知识共享协议是什么?
除了一些超过了时限的作品之外(或者公共领域),你还有另外一种获得他人作品的方式,那就是知识共享协议 (Creative Commons)。
CC 协议有四种限制条件,分别是:
署名 BY:必须按照作者或者授权人指定的方式进行姓名展示。
非商业性使用 NC:不得以商业目的而是用作品。
禁止演绎 ND:不得改变,修改此作品。
相同方式共享 SA:如果你需要更改此作品,必须使用相同方式来进行分享。
其中非商业性使用和相同方式分享相对不好理解一些。
非商业性使用 的界限有点模糊,但尽量注意不要通过他人的作品来牟利。
相同方式共享 就狠一些,是具有“传染性”的,一旦你使用了 SA 的作品,你的创作也必须按照 SA 的要求发布。这样未来如果有其他人要使用你的作品,也必须使用 SA 的格式。
这一点的良好范例参见开源软件许可证中的 GPL 许可证。
顺便一提的是,我的 blog 全站都以 BY-NC-ND 的方式进行共享。
CC0 是什么?
CC0 的意思是放弃所有权利,甚至包括署名权。你可以自由的使用这些资源,并随意的修改或者商用。
以下我会介绍一些无版权或者 CC 协议的资源网站。
图片
unsplash
unsplash
毫无疑问,unsplash 是目前最火的 CC0 图片网站,你可以随意取用,而且质量极高。
还有一些网站供你选择:
https://pixabay.com
https://pexels.com
https://visualhunt.com
https://stocksnap.io
背景图
https://freerangestock.com/
视频
videvo 提供免费的视频可以使用。
类似的网站
https://www.videezy.com/
https://www.pexels.com/videos/
https://www.openfootage.net/
https://www.vidsplay.com
Life of vids
特效
https://footagecrate.com/
音乐
国际乐谱库 收集了公有领域的音乐。
https://freesound.org/
http://freepd.com/
https://incompetech.com/
https://www.premiumbeat.com/zh…
https://www.bensound.com/
https://freemusicarchive.org/
https://www.jamendo.com/
音效
爱给素材
站长素材-音效
插画
插画
unDraw 提供了很多组人物插画,供你选择,都是矢量的,很容易修改。
Illustration Gallery也有类似的资源。
符号标志 icon
这个在 ui 设计中经常会用到,这里推荐几个网站。
icons8,提供了海量的免费 icon,但是请注意,使用他们的 icon 需要遵守免费许可,你需要在网站底部或者 app 中加上 icons8 的链接。
material这是 google 官方出的 icon,是开源的,你可以随便使用。
svgrepo :支持svg格式的icon
REMIX ICON
https://www.easyicon.cc/
https://www.flaticon.com/
游戏图标
单独说一下 iconfont。 阿里妈妈推出的 icon 共享平台,但是法律说明写道:
用户如需出于商业目的使用iconfont平台任意个人公共库中的任意图标,均应事先获得相关知识产权权利人的授权,以避免产生不必要的纠纷。
iconfont平台提供的官方图标库仅供广大用户交流学习使用,未经iconfont或其关联公司书面授权许可,不得用于任何商业用途。
尽管名气很大,但这里的 icon 是不能随便用的。
字体
首先,微软雅黑商用是需要授权的,不要以为 Windows 系统里面自带了就可以随便用。
那么有哪些字体可以直接免费使用呢?
文泉驿系列**:文泉驿是最早期的开源汉字字体了,早期使用 linux 的朋友肯定印象都很深刻。
思源系列**:这是 adobe 和 google 合作推出的开源字体,包括思源黑体,思源宋体,思源柔黑体。
站酷系列**:这是站酷推出的免费授权字体,包括了站酷酷黑体、站酷高端黑体、站酷快乐体、站酷小薇LOGO体、站酷意大利体。
得意黑**:这是我关注的设计师 @oooooohmygosh 主导设计的一款斜体艺术字体,我很喜欢。
方正免费字体:方正黑体、方正书宋、方正仿宋、方正楷体 四款字体在授权下,可以免费商用。
冬青黑
锐字真言体
大公司出的字体
阿里普惠体
金山云技术体
手机厂商推出的字体
在22年,越来越多的厂商开始推出自己的免费字体了,大家可以按需使用:
荣耀 HONOR Sans
小米 Mi Sans
OPPO Sans
华为 HarmonyOS Sans
最后的最后
尽管收集了一大堆资料,但是大家在使用前,务必确认好资源网站的声明,避免踩坑。
29 December 2022 收集了群友 @方大落 提供的地址,做了一些更新。
« 上一页
Mac优秀软件推荐-Itsycal
下一页 »
Excel 常用函数 一
昵称
邮箱
网址
欢迎评论
0 字
评论
按正序按倒序按热度
Powered by Waline v2.15.8
© 2023 kok的笔记本 Powered by Hugo & PaperMod
kok的笔记本
关于我
Pegboard
周刊
播客
分类
系列文章
搜索
如何挑选一个电脑电源?
七月 28, 2019
· 3 分钟 · kokdemo
目录
再强悍的硬件配置,没有合适的电源也不行。
之前自己在配置电脑的时候,经常会遇到一个问题:
市场上这么多电源,应该怎么挑选呢?
于是今天整了一下当时的笔记,发出来供大家参考。
一、那些参数要关注?
对于装机的朋友们来说,电源只有三个参数值得注意一下:
能否装进机箱里
功率能否带动硬件
发热控制和噪声控制
二、尺寸
装机时首先要考虑的就是尺寸了,如果是 ATX 或者 M-ATX 机箱,体积都很大,可以随便买,对于小一些的 ITX 机箱就需要单独来看了。
ATX
标准尺寸为 150 * 140 * 86 mm,其中会搭载一个 12cm 的风扇。
SFX
标准尺寸是 125 * 100 * 63.5 mm,由于尺寸的限制,SFX 电源一般只能塞一个 9cm 或者 8cm 的风扇。
FLEX/1U 电源
这个本来是给服务器上用的,所以造型和之前的不同,为 81.5 * 150 * 41.5 mm 是一个长条型。
由于尺寸的限制,能放进去的风扇就更小了。
三、功率计算
如果不超频的情况下,电脑的功率计算公式大概等于:
总功率 = (CPU 功率 + 显卡功率)* 1.2
CPU 功率可以参考 TDP(热设计功耗),也就是 CPU 在满负载情况下产生的功耗,你的电源需要至少支持这个功率。
显卡的计算也类似。
至于多出来的 0.2 ,主要是算到内存硬盘外设等其他不太吃功率的小户上了。
如果你超频,那么这些值就需要重新计算了。
功率计算器
更容易的办法是直接用计算器,这里推荐几个:
微星提供的电源计算器:https://cn.msi.com/power-supply-calculator/
航嘉提供的电源计算器:http://www.huntkey.com.cn/service/calculator.html
功率不足会怎么样?
正常情况下使用应该能开机,一旦遇到 CPU 或者显卡高负载的时候,立马宕机重启。
四、发热控制和噪声控制
这两个问题其实是一个问题。
发热量大了 -> 提升风扇转速 -> 噪声大了 -> 换大风扇/降低转速
所以先从发热控制说起。
80Plus
这是一项美国电力协会制定的方案,通过这个认证的电源,在高负载情况下,转换效率要大于 80%。
转换效率大于 80%,也就是说会有小于 20% 的电能会被转会化成热能。
这个值当然越低越好,所以这个标准一共包括了 6 个等级。
{{% table /*%}}
等级 高负载热效率
普通 80%
铜牌 82%
银牌 85%
金牌 87%
白金 89%
钛金 90%
{{%/* /table %}}
这里的高负载指的是满负载的情况下的热效率。可以看出对于普通用户来说,由于开机时间短,任务简单。
选择钛金和普通的电源,热效率也就差了 10%,节省的电费大概一个月几块钱吧。但是购买的时候,钛金标准的电源要贵好几百呢。
当然,节省的电费只是一小部分的问题,由于热效率提升了,电源自己就不会产生更多的废热了。
产生的热量更少,对应的噪声也就更小了。
风扇
电源的风扇是电源唯一的散热渠道,所以挑选一个大一些的电源,散热能力会比小的好很多。
即便同样的散热环境下,大的风扇可以用更低的转速达到减少噪音的效果。
这里提一下 SFX-L 这个电源,比 SFX 大一些,可以放一个 11cm 的风扇进去,就会比 SFX 的散热好很多。
五、线缆
这是一个题外话,电源分为非模组化,半模组化,全模组化三种。
全模组化的意思是,你可以选择插那几个电源线,不用的完全可以不插。
这样的好处有两个:
少插一些线,机箱走线更清爽
可以换自己定制的线,更美观
我这次组装的电脑,就购买了全模组的电源,但是电源自带的线实在是太硬了,在机箱中走线的时候痛苦到怀疑人生……
六、总结
总结一下,挑选电源的步骤如下:
确定机箱尺寸,然后确定电源尺寸
计算硬件总功率,确定电源的总功率
根据自己的需求,选择对应热效率的款式
预算范围之内,尽量买好一点的,不要挑选杂牌子
装机
« 上一页
钢铁、蒸汽与 19 世纪 聊聊《铁道之旅》
下一页 »
有用的正则表达式
昵称
邮箱
网址
欢迎评论
0 字
评论
按正序按倒序按热度
Powered by Waline v2.15.8
© 2023 kok的笔记本 Powered by Hugo & PaperMod
kok的笔记本
关于我
Pegboard
周刊
播客
分类
系列文章
搜索
有用的正则表达式
七月 26, 2019
· 2 分钟 · kokdemo
目录
工作中总会用到一些使用正则表达式的时候,这个东西又特别难记,所以新开一篇文章专门放自己常用的正则表达式。
正则表达式英文叫 “regular expression” ,直译过来就是规律表达式的意思,不知道第一个这么翻译的人是谁。
一、正则表达式学习教程
推荐 正则表达式30分钟入门教程
如果只是快速查一下表,可以看这个 正则表达式速查表
二、正则表达式匹配工具
写完正则表达式后,一般都需要测试一下,不要直接扔到测试环境里。
你可以用
[https://regex101.com/]
[https://deerchao.cn/tools/wegester/index.html]
来检查自己的公式。
如果你写的是js版本,你可以用 [https://regexper.com/] 来可视化的看自己的公式。
这里推荐一下:[http://wangwl.net/static/projects/visualRegex/],也是一个好用的可视化工具。
可视化工具
三、正则表达式在工具中的使用
3.1 VS Code
这是非常常用的编辑器,使用正则也很简单,按下 command + f 打开文件搜索,点击搜索框最右边的icon,激活正则表达式模式。
VS Code
这样第一行就能输入正则表达式,匹配之后还可以在替换中替换字符。
3.2 JavaScript
在js中使用正则表达式也很简单,首先创建表达式字面量。
let regex = new RegExp(“ab+c”);
然后可以调用一些方法来使用。
var result = regex.exec(“your input text”);
// 如果匹配到返回一个数组
同理,还可以使用 test,match,search,replace,split 方法。 具体的使用请看MDN的这篇文章。
3.3 Office Word
很神奇,word竟然也支持,只要在替换的高级搜索中,勾选“使用通配符”,就可以使用了。
Word
至于同厂的 Excel ,竟然不支持这个功能,只能通过内嵌的VBA来实现。这里就不多说了。
此外,Mac OS 自带的 Number 和 Page 均不支持此功能,死心吧。
四、一些常用的正则表达式
这里后续会陆续更新一些内容进来。
4.1 匹配空行
^\s*(?=\r?$)\n
^ 代表一行开始,\s*0个或任意空白字符,\n匹配换行符。
(?=\r?\()复杂些,\r?匹配0个或1个回车符,\) 代表行尾。(?=)代表正向匹配预查,匹配后面紧跟着的\r?$的^\s*。
含义是这个含义,至于怎么构造出来的我就不太理解了。但是确实是有用的。
好啦,最后祝大家高效工作少加班。
正则表达式
« 上一页
如何挑选一个电脑电源?
下一页 »
给psd瘦身
昵称
邮箱
网址
欢迎评论
事实上,每个标签有成千上万的笔记
闪念笔记:24小时法则
用碎片时间快速记录灵感(手机便签/语音备忘录),但每天固定时间清理:
有价值 → 升级为文献/永久笔记
无价值 → 直接删除(防止信息堆积)
文献笔记:强制转述对抗浅层学习
读完一段内容后,合上书本,用自己的话重写核心观点(参考费曼技巧)。
必须标注来源(例:@Kahneman2011, p.56),便于后期追溯。
永久笔记:用提问驱动思考
书写时自问:
这个观点可以解决什么问题?
它与我的哪些现有知识冲突或互补?
如果结论相反,会发生什么?
轻量至上,避免工具沉迷
纸质党:A6卡片+实体卡片盒(优点:触觉记忆强化思考)。
数字党推荐组合:
收集:Readwise(自动同步阅读批注)
处理:Obsidian(双向链接+图谱视图)
输出:Scrivener(长文写作管理)
警惕:工具功能越复杂,越容易陷入“整理强迫症”,记住卡片盒的核心是思考,不是分类。
卢曼本人的杀手级习惯
每天写6条笔记:不求数量,但每条必须达到出版级质量。
反向提问法:假设笔记是答案,逆向推导它可能回答的问题。
跨盒链接:卢曼实际使用两个卡片盒——
文献盒(引用他人观点)
主盒(个人思考产物),通过编码系统交叉引用。
从知识管理到创新引擎
矛盾碰撞法:故意链接两个对立观点(例:“计划经济效率低下” ↔ “中国高铁成功案例”),激发新假设。
空白卡片策略:在卡片盒中插入空白编号(如预留1a1到1a10之间的空位),暗示该主题有待探索。
最小可行输出(MVO):当某个主题链接超过20条笔记时,强制输出一篇800字短文,验证知识密度。
避坑指南:90%初学者常犯的错误
过度分类:建立“心理学/经济学/哲学”文件夹 → 知识割裂,改用标签动态管理。
摘抄病:直接复制原文到永久笔记 → 失去转述的理解检验作用。
链接泛滥:每条笔记链接50+相关笔记 → 失去重点,优先连接强相关(3-5条最佳)。
实践检验:你的卡片盒是否健康?
健康标志:随机打开一条笔记,能通过链接在3步内跳转到看似无关的领域。
异常信号:超过30%的笔记无反向链接,或长期存在孤立笔记集群
,为了明确文献卡片应该记录什么,阳志平老师的卡片分类提供了很好的参考。他将卡片分为七种类型:
术语卡:阅读中出现过的学术术语或者作者特定黑话;
人名卡:阅读中出现过的人名,以及他的个人简介如何;
反常识卡:有什么理论模型/推断证据/故事/行动,挑战了你的既有常识;
金句卡:收集性感的句子;
行动卡:写下你可以执行的行动;
技巧卡:积累你学到的技巧;
任意卡:此处自行发挥。
谁在用 Zettelkasten 系统?
德国作家迈克尔·恩德(Michael Ende)拥有一个 Zettelkasten,1994 年,在他去世的前一年,出版了《迈克尔·恩德的卡片盒:草稿和笔记》,这是一本选集,这本书收录了他的一些文章以及卡片中的洞察和箴言。
美国作家安妮·拉莫特(Anne Lamott)在她的《Bird by Bird》一书中用了整整一章来介绍索引卡。澳大利亚作家凯特·格林维尔(Kate Grenville)在《The Writing Book》的一章中专门介绍了使用 “成堆 “的笔记作为写作过程的一部分,她说,编剧经常使用索引卡来组织他们的脚本。
二十世纪的美国喜剧演员菲利斯·迪勒(Phyllis Diller)有 52000 张 3×5 英寸的索引卡,琼·里弗(Joan Rivers)斯有超过一百万张 3×5 英寸的索引卡,鲍勃·霍普(Bob Hope)有 85000 页的文件,乔治·卡林(George Carlin)也因拥有许多写满笑话的卡片而闻名,他们经常用碎纸片、收据、洗衣单和火柴盒来记笔记。
美国总统罗纳德·里根(Ronald Reagan),将他经常用于演讲的名言警句保存在一个卡片集里。
嘻哈歌手埃米纳姆(Eminem)在《60分钟》节目中接受安德森·库珀的采访时,展示了他装满废纸、酒店的文具、散页纸和几张黄色法律便笺的滑轮箱,其中包含单词、短语、歌曲、诗歌和笔记,保存在一个有硬边的大箱子里。
去年,和一个朋友闲聊,他说卡片盒笔记法让他想到了唐代诗人李贺,李贺又被称为布袋诗人,《新唐书 · 李贺传》中有这样的记载:
李贺字长吉为人纤瘦,通眉,长指爪,能疾书。每旦日出,骑弱马,从小奚奴,背古锦囊,遇所得,书投囊中。未始先立题然后为诗,如他人牵合程课者。及暮归,足成之。非大醉吊丧日率如此,过亦不甚省。母使婢探囊中,见所书多,即怒曰:“是儿要呕出心乃已耳!”
——《新唐书·李贺传》
译文:李贺,字长吉,长得单薄削瘦,双眉相连,手指很长,能快速写出诗文来。每天清晨太阳刚刚初升时,就骑着一头瘦弱的驴子,带着一个小书童,背着又古又破的丝囊,碰到有心得感受的诗句,就写下来投入丝囊中。不曾有过先确定题目再写诗的事,就像别人按题目牵强附会写作那样。等到晚上回来,就整合成一首诗。若非大醉时或吊丧的日子他都是这样,每天到母亲处探望也顾不得问候一声。他的母亲让婢女拿过锦囊取出里面的草稿,见写的稿子很多,就心疼嗔怪道:“这个孩子要呕出心肝才算完啊!”
李贺觅诗图
这个典故刚好是成语“呕心沥血”中的“呕心”的出处。从记载的描述看,李贺是先收集素材,后创作诗歌,而不是先命题,再遣词造句牵强附会。李贺的创作方式是一种自下而上的创作方式,这正是卡片盒笔记法的特点。若不是英年早逝,李贺一定能创作出更多脍炙人口的诗歌。
卡片盒笔记系统最重要的特点:
它是如同网页一般的超文本;
它坚持笔记原子化的原则(Atomicity);
它是个性化的;
首先,它不是孤立的文本,也不是大量文本的集合(collection),它是某种超文本,是可以相互参考、相互解释、相互拓展、相互利用信息的文本。常规的笔记系统与 卡片盒笔记系统的区别在于,卡片盒笔记系统非常强调形成笔记间的关系,它优先考虑连接(connect),而非收集(collect)。
要成为超文本系统,卡片盒笔记系统需要多个文本或者说是笔记通过超链接(hyperlinks)互相连接。我们把一个单独的笔记叫做 “Zettel”,”Zettel” 在德语中是纸条的意思,它是 卡片盒笔记系统中最基本的组成部分。
每一条笔记都需要一个唯一固定的地址,我们可以通过这个地址来引用它,以及在笔记卡片之间建立连接。这样一个超文本的笔记系统就可以称得上是卡片盒笔记系统了。
其次,卡片盒笔记系统需要遵守原子化的原则(每条笔记包含且只包含一条知识(one unit of knowledge)。为了弄清楚原子是什么,我们可以问一问自己,我们想用原子化的笔记创造的分子是什么样子的时候,你能大概体会什么是原子。
译者注:
我个人的观点是,原子化的笔记就是一条想法(One thought),而卡片盒笔记系统就是由想法组成的想法之网(web of thought)。
thought 这个词翻译成想法是不得已而为之,因为 thought 含义实在太广,从个人经验来看,thought 可能是一个术语(term)、一个实例(example)、一个主意、一个观点等等,我只有将其翻译成想法可能才更贴切。
我们先看一下没有以单条想法为原子的例子。我们常见的书本,它的最小信息单元是页(page),页又组成了节(section)和章(chapter)。我们可以通过页码(address)找到任何的章、节和页,但是你很难通过页码引用(refer to)书中的某条想法。因为这条想法可能分布于书中的任何地方,没有专门的页码指向它。
维基百科也不是思想之网,因为在一个页面内,你通过超链接只能链接到其他页面里的整篇文章或者页面内的章节,无法链接到该页面中的单个的想法。因为,文章和章节有地址(网址),想法是没有被分配地址。维基百科是一部百科全书,每篇文章都包含某个主题的信息。它是一个信息检索工具,而非思维工具(thinking tool)。
相比之下,在卡片盒笔记系统中,直接引用某条想法是可行的。你只需引用某个原子化笔记的地址即可引用某条想法,因为一条原子化的笔记中有且只有一条想法。
卡片盒笔记系统是一个思维工具,因此它需要把单个想法作为基本单位。为了连接 想法,要给每个想法分配一个地址以供引用。用 Zettler 的话说就是:一条笔记,一个想法。
最后,卡片盒笔记系统不是标准化的工业品,它是一个非常个性化的,属于你个人的工具。独自思考和与他人交流是不同的过程,你的卡片盒笔记系统应该是你个人的思考工具。
不过,这不意味着创建一个共享的,基于特定项目的超文本系统没有意义,但这不是本文定义的卡片盒笔记系统。
There are two different types of tags:
Tags for topics. You use tags to group notes under a topic.
Tags for objects. You use tags to group notes around an object, real or conceptual.
The difference between these types of tags is very easily understood. If I use tags for topics I would tag everything that is relevant for the topic of diet with diet. A note about carbohydrate intake and insulin sensitivity would definitely fulfill this criterion. If I use tags for objects, I would only tag notes with diet when these notes are specifically on the concept of dieting. I would not tag the note on insulin sensitivity with diet. But I’d use the tag for a note on metabolic flexibility as overarching concept that connects diet and fasting.(1)
靈感筆記(Fleeting Note)
「靈感筆記」(Fleeting Note)指的是你對於讀到、聽到、想到的任何資訊,產生想法之後,進行快速紀錄的筆記。例如你吸收了文章、書籍、影片、Podcast、與人談話、演講…等,當下得到的靈感或想法,也可能是散步或做家事時的靈光乍現。
如何捕捉到更好的靈感筆記?你可以嘗試帶著「輸出的前提去輸入」,在吸收資訊時心裡想著:「當我吸收這些資訊之後,我要寫出一篇可以公開發表的筆記(或文章)」,這會讓你在吸收資訊的時候更專心、更有目標性、更想要理解透徹。
你可以用手寫的方式寫在卡片或筆記本上,也可以用錄音或錄影的方式記錄在手機裡面。內容可以是:零碎片段的重點內容、值得記錄下來的事情、倏忽即逝的靈感、尚未成熟的想法。
不用遵守什麼特定格式,也不需要有完美的脈絡,靈感筆記不是用來記錄細節,而是用來「提醒」未來的你,曾經在腦袋裡面想過哪些事情。靈感筆記採用的「媒介」本身並不重要,只要能快速捕捉、容易檢視、定期回顧,就是好方法。
2.文獻筆記(Literature Note)
「文獻筆記」(Literature Note)指是當你在閱讀文章和書籍、收看影片、聆聽 Podcast 和演講的時候,在一張紙上記錄的內容(當然也可以用數位筆記做紀錄)。
用條列式的方式,寫下這則文獻帶給你的想法、值得紀錄的重點、以後會用到的素材,然後在筆記的尾端註明詳細的文獻來源細節。註明清楚文獻的出處,是為了讓未來的自己和讀者,對這些內容得以進行檢驗和驗證。這也是一種筆記內建的回饋機制。
撰寫 Literature Note 的時候,需要注意這四點:必須要很有選擇性地決定寫下哪些內容、文字要簡短扼要、用你自己的話描述而非照抄(加深記憶力的關鍵:不要求記得,而是求理解)、註明文獻的名稱、作者、年份和頁數。
文獻筆記的格式
我在閱讀《How to Take Smart Notes》的時候,心中一直有一個問題沒有獲得解答:「Luhmann 到底是怎麼寫 Literature Note?」我後來在專門討論卡片盒筆記法的德國論壇的一篇討論串找到了相關的細節1。Luhmann 在閱讀任何書籍和文獻時,並不會直接在紙上劃線、也不會在邊緣寫字。他會把任何值得紀錄的、當下的靈感、自己產生的想法,寫在另外一張 Literature Note 上面。
短篇幅的 Literature Note 的筆記上面只有一個想法,在底端寫下文獻的「名稱、作者、年份和頁數」。
中等和長篇幅的 Literature Note 則是標註「頁數、想法」,每一條重點只有一到兩行的文字敘述,然後一連串排列下去。
不同於 Permanent Note 採取「橫式」的方式書寫,Luhmann 的中、長篇 Literature Note 是採取「直式」的筆記紙來書寫。
正確使用卡片盒筆記法(Zettelkasten)的六個關鍵步驟 2短篇幅文獻筆記
正確使用卡片盒筆記法(Zettelkasten)的六個關鍵步驟 3中等篇幅文獻筆記
正確使用卡片盒筆記法(Zettelkasten)的六個關鍵步驟 4長篇幅文獻筆記
然後,他會把這些筆記全部收納在另一個卡片盒裡(Literature Slip-box),以文獻作者名稱的字母排序。如同他在這篇文章〈Learning How to Read〉說過的:「我們要怎麼記得自己讀過的東西?其中一種可能是,用人物的名字排序來記憶。」2
另外,Luhmann 在寫 Permanent Note 的時候習慣以「人名、年份、頁數」的順序註明文獻出處,這也顯示了他書寫和使用 Literature Note 的流程。
3.永久筆記(Permanent Note)
「永久筆記」(Permanent Note)可以翻譯成,這則筆記本身,只能代表一個完整的想法,一則筆記就代表一個脈絡(context)。在不需要多餘的前、後文補充之下,你就能夠看懂這則筆記的內容。
寫 Permanent Note 的時候,要假裝你是寫給「別人」看的。用完整的句子、揭露你想法的來源、寫下明確的文獻、試著寫得精確且簡短。
第一步,是透過回顧,去「轉化」那些你曾經寫過的 Fleeting Note 和 Literature Note,去思考它們跟你現在的思考和興趣有什麼關聯。撰寫的時候不要只是「蒐集」那些別人講過的話,而是去「發展」自己的論點,跟其他的想法進行「討論」。Literature Note 是條列式的重點文字,Permanent Note 比較像是書寫的散文。
完成之後,你就可以把 Fleeting Note 丟棄,或者把 Literature Note 收藏到專門存放文獻的卡片盒(如果你使用數位筆記,可以歸到一個特定的分類,或者收進專門掌管文獻的軟體例如 Zotero)。
第二步,是把這個寫好的想法,跟「舊的」Permanent Note 建立「連結」。找出相關的舊筆記,然後問自己以下三個問題,並且把答案寫在新的筆記上:
新筆記是否能夠反駁、更正、支持或新增到我已經知道的事情?
我如何組合這些想法,產生出一個新的想法?
這些新的想法,觸發了什麼新的問題?
隨著卡片盒裡面的筆記數量不斷增多、彼此的連結不斷被建立起來,它的價值才會真正被顯現出來。如同 Luhmann 曾經說過:「每一則筆記都是一個元素,它的品質高低,只來自於它在系統裡的網路連結和反向連結。」3
如果你沒有替這則筆記建立任何連結,它遲早會被你遺忘掉,且不具備任何價值。
文獻筆記和永久筆記的關係
全新的真知灼見不會天天發生,靈感也不會每天到來。但我們可以透過「轉化」文獻筆記的過程,去提高自己發現洞見的機率。作家 Eva Keiffenheim 曾經在一篇文章分享過,她如何把 Literature Note 轉化成 Permanent Note 的方法。4
「Literature Note」vs.「Permanent Note」的關係:
事實和理論 vs. 這個事實和理論與你生活、工作、好奇心的關聯
作者對於事實和理論的解讀 vs. 你對於作者對於事實和理論的解讀
回答作者問題的代表資訊 vs. 回答你自己問題的代表資訊
你需要原始資訊才能瞭解這則筆記 vs. 你不需要原始資訊就能瞭解這則筆記
你從文獻的文字上重述想法 vs. 你從自己的心裡重述想法
我認為,這是一個非常實用的轉換方式,將焦點放在「自己」身上,把文獻的所見所聞,轉化成跟自身相關的筆記。透過這樣的過程,就能夠把吸收的資訊「內化」成自己知識庫的一部分。
值得一提的是,Permanent Note 並非寫了一次之後就不能修改。Permanent Note 也是一個「過程」,你在日後都可以隨時更新這則筆記。如同 Eve 說的:「永久筆記真正的意義是它對你永久『有用』。」把 Permanent Note 當成一個會隨著你一起成長的東西。
用「別人」看得懂的文字書寫,用跟「自己」相關和感興趣的事物去連結。
4.索引卡片(Indexing Card)
Luhmann 對 Permanent Note 的分類方式,並不是用傳統「主題式」的字母排列。他採用的是「索引編號」(Numeric Index)來替每一張卡片命名,然後寫下這張卡片跟其他卡片的連結。這種對卡片的命名方式稱之為「索引卡片」(Indexing Card)。
如下圖所示,其中一則筆記命名為「1」,如果他下一次新增的筆記跟「1」有關連,就接續在後面命名為「1a」。如果新的筆記跟「1」的關聯比較強,他會命名為「1b」;如果跟「1a」的關聯比較強,他就命名為「1a1」。他這些卡片上面,還會寫上連結到其他卡片的索引,例如在「1」的卡片裡面,可以寫上連結到「2a2」或任何一張卡片(就像手寫版的超連結)。
實體卡片盒和筆記紙的用途
Luhmann 的卡片盒擁有數萬則的筆記,他是如何「回想」自己寫過的筆記?當他要直接找出一則筆記的時候,他怎麼知道要「打開」哪一個卡片盒抽屜?他怎麼知道要「抽取」卡片盒裡的哪一疊筆記?
關於筆記的提取方式,《How to Take Smart Notes》書中並沒有特別著墨,因此我在把卡片盒筆記法轉換成「數位卡片盒筆記」的過程中,遭遇了一個問題:「數位筆記的強項就是超連結,看起來就不需要替每張卡片做索引編號了。但是,如果我單純仰賴『關鍵字』去回想筆記,這種方式可靠嗎?」
的確,單純仰賴關鍵字的回想,就像在一片散亂的拼圖裡面,試圖找到正確的那一片。我認為,Luhmann 回想他的筆記之所以快又有效,必定存在著除了關鍵字之外的元素。後來,我想起了探討學習與記憶的《最高學習法》這本書中,有一項關於記憶的重要元素:「空間位置」。
我認為,筆記放在卡片盒的「實體」空間位置,很重要。
空間可用於引導提示,幫助回想
人類大腦有一個叫做「海馬迴」(Hippocampus)的部位,這是一個形狀很像海馬的組織,位在耳朵後面的大腦中央。人類為了形成長期記憶,訊息必須經過海馬迴上面數百萬計的微小組織「位置細胞」,這些位置細胞會把我們形成記憶時的空間位置,埋藏在每一個新生成的記憶裡面。
就像你在讀一本紙本書的時候,紙頁上的資訊會存在一個明確、靜態、不會改變的三維空間裡。例如書中的某一句名言,可能印在這本書大約四分之一厚度的紙上、靠近頁面頂端的位置。當你試著記憶這句話的同時,你也把它在書中的位置記了下來。日後,如果你要回想這句話,那麼這個位置,就成為了這項回憶的引導提示。
因此,空間位置可以作為一種引導提示,幫助我們回想過去曾經學習和經歷過的資訊。我認為,筆記被放在卡片盒的位置,就是記憶的一部分。單純的索引編號不會讓我們形成深刻的記憶,但是筆記和卡片盒的位置可以達到這個效果。
卡片盒筆記法的索引編號有什麼用途?
根據標準的卡片盒筆記法,我們必須對每一張筆記分配一組「獨特且唯一」的索引編號,例如 1/a, 1/a1, 1/a2, 1/b …等。我認為這個做法有兩個用途:
第一個最顯而易見的用途,就是建立紙本筆記之間的「超連結」。Sönke Ahrens 和其他學者認為這是最重要的目的,這個索引編號就像是筆記的身分證號碼,可以讓 Luhmann 快速找到每一張實體的筆記卡片,也可以在不同筆記之間,建立起任意的超連結。
第二個用途則鮮少被提到,那就是用來回想筆記的「空間位置」。想像今天有一個編號 7 的卡片盒,它的前幾張卡片代表某一個學科領域。如果我們新增了一張編號 7/1a 的筆記,放到對應編號也是 7 的卡片盒裡面,塞到盒子前緣三分之一的位置。之後,當我們要在回想這一則筆記時,就可以藉助卡片盒實體的「空間位置」作為引導提示,在整櫃卡片盒右下角的盒子裡,前端的三分之一處,就有我們想要找的那張卡片。空間可用於引導提示,幫助回想。
因此,當我們採用「數位筆記」的方式來做卡片盒筆記的時候,雖然不用再煩惱超連結的問題,可是真正有助於回想筆記的關鍵,正是筆記的空間位置。人腦很不擅長處理一堆數字和零散的關鍵字,但是,一旦這些資訊跟空間有了連結,回想和記憶就顯得容易許多。想要發揮卡片盒筆記法的全部潛能,挑選一個內建「空間功能」的數位筆記是非常重要的。
5.關鍵字索引(Keyword Index)
由於卡片盒筆記法的 Permanent Note 是用依照「索引編號」(Numeric Index)的字母順序來排列,主題的分類是「由下而上,而非由上而下」。在這種缺乏固定層級(fixed hierarchy)的主題結構下,我們該如何回想起特定的筆記呢?
除了卡片盒筆記法的三種筆記之外,《How to Take Smart Notes》書中甚少著墨的「關鍵字索引」(Keyword Index)筆記,反而是 Luhmann 能夠有效回想其它筆記的關鍵元素。根據統計,在 Luhmann 龐大的 90,000 則筆記當中,就有 4450 張屬於 Keyword Index 的用途。
Luhmann 會在一張 Keyword Index 上面,寫下某一個主題,然後把跟這個主題有關的 Permanent Note 「筆記名稱、連結」用索引的方式條列出來。例如下圖的例子:Rh – Ri,裡面就有好幾條關於「風險」(德文 Risiko)的筆記索引項目。每一張 Keyword Index 可能會記錄 3~10 條不等的筆記索引,在日後還可以陸續增添新的索引上去。你仔細還會發現,跟風險相關的這些筆記,其連結「最前面的索引數字」,彼此距離都非常遙遠(3, 7, 21, 32, 54, 333)。5
正確使用卡片盒筆記法(Zettelkasten)的六個關鍵步驟 6Keyword Index
正確使用卡片盒筆記法(Zettelkasten)的六個關鍵步驟 7Keyword Index of Rh – Ri
這種列出關鍵字索引的方法,確保了他每一次對關鍵字的「搜尋」,都會開啟一片廣泛的筆記「網」。透過這些索引,他可以快速找到「跟關鍵字相關」的筆記,而這些四散各地的筆記,剛好又勾起了不同「索引脈絡」之下的回憶,讓他在各種異質層面之上對這些筆記做出新的連結。
從筆記的關鍵字分類看出端倪?
在讀《How to Take Smart Notes》的時候,我心中有一個問題「Luhmann 是怎麼決定卡片的分類? 」如果,單純仰賴每一張 Permanent Note 的「索引編號」,那麼多繁雜的英文和數字,我不相信他單純依靠數字或字母來回想起不同的分類。關於這個問題,我們可以從 Luhmann 龐大的 90,000 則筆記的「類別數量」找到一些線索。
他的第一組卡片盒筆記可以追溯到 19521961 年間(他大約 2535 歲),涵蓋的學科領域包含了 108 種分類。筆記的模式大部分是在探索和反思,特別是針對一些已經預定的、詳細的知識領域,例如國家、平等、計畫、全力、憲法、革命、階級、科學、角色、世界的觀念、資訊…等。
第二組卡片盒筆記可以追溯到 1962~1997 年間(他的餘生),這組筆記的設計更加地「問題導向」,也很明確地圍繞著社會學這個領域,其結構和第一組卡片盒筆記有相當大的不同。第二組的最高層級學科領域,只有 11 種分類,包含了組織理論、功能主義、決策理論、正式/非正式秩序、主權/國家、個體概念/個體問題、經濟、臨時筆記、古代社會、先進文明。從這些分類當中,我們可以發現它沒有必然的順序和層級,也不像是一本書的目錄;相反的,它們只是 Luhmann 閱讀和研究興趣的歷史產物。
Luhmann 在這兩個階段的演進順序,讓我回想起《跨能致勝》這本書中提到「通才」型人才的優勢:在早期先培養廣泛興趣和多元涉獵,反而能養成日後觸類旁通的能力,解決「專才」難以解決的跨領域問題。他透過早年的廣泛探索和反思,循著興趣前進,然後逐漸聚焦到社會學領域進一步深耕。
不用去苦惱分類的「數量」該有多少,而是透過關鍵字索引,建立起回想、搜尋和連結的「脈絡」。
6.如何用卡片盒筆記法撰寫文章
當我把這套卡片盒筆記法整合到自己的「寫作流程」時,會是什麼樣子?
答案就是你正在讀的這篇文章。
在撰寫這篇文章的時候,我就是用卡片盒筆記的方式來整理所有的資訊,使用的是 Heptabase 這套台灣新創團隊的數位筆記工具。我在自己的線上課程《化輸入為輸出》教授的三步驟是輸入、整理、輸出,是對於「單一」資料來源十分有效的方法。而這篇文章,則是廣泛閱讀了「多篇」相關文獻,通盤瞭解之後才整理出來的內容。以下是我實際進行的步驟。
正確使用卡片盒筆記法(Zettelkasten)的六個關鍵步驟
我透過 Heptabase 使用卡片盒筆記來寫作的軟體畫面截圖
首先,我把自己內心關於卡片盒筆記法的疑惑,全部寫在子彈筆記本和 Heptabase 的 Timeline 上面,目的是快速捕捉。每一則手寫下來的問句或想法,或者是透過 Heptabase 捕捉下來的筆記,都可以視為一個獨立的 Fleeting Note 卡片,這個時候的內容非常粗糙。
接著,針對每一個未臻成熟的卡片,我去尋找相關的文獻來閱讀,然後把那些資料寫成 Literature Note。這時候,我是主動篩選閱讀的資訊,只把那些能夠解決我疑惑的資訊記錄下來,用自己的話闡述一次,如同示意圖右下角「藍色」的卡片。
然後,將每一張卡片,陸續轉換成 Permanent Note,用自己的邏輯,重新排列組合那些 Literature Note 提供的資訊,然後寫成一則又一則的卡片筆記,如同示意圖右下角「白色」的卡片。這些筆記,就是之後寫這邊文章的主要素材。
接下來,我在 Heptabase 的 Whiteboard 面板上面,隨意手動拖曳,進行排列組合,把每一則筆記放到適合的位置,形成一個有架構、有脈絡的卡片組合。這個做法,體現了 Indexing Card 強調「空間位置」的特色,就像是把實體的卡片放在桌面挪動一樣。在這個過程中,我對於每一則筆記之間的關係,又有了更深刻的認識,也讓我產生了更多 Fleeting Note 的想法,尋找更多 Literature Note 的補充資訊。上述的步驟是一個「動態」的反覆流程,而不是線性的固定順序。
最後,經過了兩週的整理,把每一則 Permanent Note 調整到語句通順的程度,然後就使用 Heptabase 的 Tab 功能,把所有的筆記陳列在右邊,把我要撰寫的這篇文章放在左半邊。接下來的工作就顯得簡單許多,我只要依照脈絡,把所有的 Permanent Note 進行線性的順序排列,微調段落之間的連接詞,就形成了這篇有結構的文章(Structured Article)。這篇部落格文章就是一張 Indexing Card 的「數位文章版本」 ,卡片頂端的大綱就是 Keyword Index,而在底下展開的完整全文,就是一套完整的思想脈絡,透過文章的方式呈現出來(這也是數位筆記的好處,內容不會被卡片大小限制)。這份 Keyword Index 如同示意圖右下角「黃色」的卡片,正是串連了所有相關筆記的主要環節。
後記:好玩的體驗
最近這一個月以來,我的文章都是使用卡片盒筆記法搭配 Heptabase 來撰寫,這也是我頭一次感覺到:寫筆記和文章的過程,可以是一種好玩的體驗(playful experience)。這種感受有別於以往枯燥的「書寫」過程,它是一種更接近於「遊玩」的本質。
採取這套方法的優點,就是讓自己「循著興趣前進」,樂於主動探索、補充、解構、重組手邊的卡片筆記。這篇長文也是因為這樣而寫出來,整篇文章的字數很多,可是撰寫的過程卻十分有趣。最後附上本文的六條重點整理,幫助你充分理解我想表達的意思。
Fleeting Note:「媒介」不重要,只要能快速捕捉、容易檢視、定期回顧,就是好方法。
Literature Note:用自己的話詳細闡述,寫下「條列式」的筆記和想法,標明出處。
Permanent Note:用「別人」看得懂的文字書寫,用跟「自己」相關和感興趣的事物去連結。
Indexing Card:索引除了用於超連結的功能,筆記放在卡片盒的「位置」,本身就是記憶的一部分。
Keyword Index:手動整理關鍵字索引的筆記卡,重點在於建立一套方便未來回想、搜尋和連結的「脈絡」。
Playful Experience:使用適合的方法和工具,筆記和寫作可以成為一種「好玩」的體驗。
如果你對於卡片盒筆記法感到興趣,可以延伸閱讀我的另一篇文章〈風靡歐美的卡片盒筆記法是什麼?12 個法則和重點整理〉。如果你對我「使用 Heptabase 實踐卡片盒筆記法」6的方式有任何的問題或建議,歡迎留言告訴我,我很期待能發揮卡片盒筆記法的全部潛力。
近期,我也寫了一篇長文〈【實踐心得】使用卡片盒筆記法兩年後,我的改變和具體收穫〉,分享了一些在我實際操作卡片盒筆記法的兩年來,我得到了哪些具體的改變,無論是心態上的、觀念上的,甚至是行為上的。歡迎前往參考看看我體會到了什麼改變。如果你對我使用的 Heptabase 有興趣,也可以前往官網免費試用七天。
彙整兩年來的實踐心法的一門課
有鑑於此,我推出了一門全新的線上課程「卡片盒筆記實戰課」,把我這兩年關於卡片盒筆記法的「How」和「What」彙整起來,分享一套完整又實際的操作方法,幫助大家達到更高效的學習和應用成果。
誰適合參加這個課程呢?如果你寫了很多筆記,但不知如何串連,這堂課很適合你。如果你覺得學過的東西總是不能實際應用,這堂課會有解答。如果你需要寫文案、做簡報或提案,但總覺得缺乏靈感,我會教你怎麼讓卡片盒筆記法助你一臂之力。如果你已經有筆記或寫作的習慣,希望更進一步提升效率和創造力,這裡有我身為文字創作者的秘訣。即使你曾經嘗試並失敗過卡片與筆記法,也不用擔心,我們會從原理和底層邏輯教起,然後帶你一步一步走過必經的關鍵步驟。
這堂課的總長 10 小時,是預錄課程可以永久重複觀看。我們會帶你走入實作,從卡片的種類、索引系統到如何有效分類和搜尋資訊,都一一剖析。我也會親自操作示範,讓你了解如何實際創建和設置自己的卡片盒系統。跟著我一步一腳印,讓你第一做卡片盒筆記就兼顧可行性和實用性。我也會分享我如何藉由 Chat GPT 的協助,優化自己的筆記和內容。
最後,我會聚焦於如何運用卡片盒筆記法提升產能和學習效率,包括如何高效組織筆記,用來寫成文章、製作簡報、研究與學習新學問。我希望透過這堂課,不僅教你捕捉和儲存知識,更教你如何運用和發展知識,讓你對知識管理的技法更上一層樓!
除了原本的正課內容之外,我還會在 2024 年舉辦 12 次線上直播講座,每個月邀請一位跨領域職人來跟大家分享知識管理的方法和訣竅。我期許讓這堂課不只幫你打基礎,更成為一個可以伴隨我們持續成長的課程。
)制作文学笔记
从你记录重点内容的页面,创建一个新的笔记页面。在这个新的笔记页面上,是用你自己的话总结出来的要点,你要在这里写下你想记住的东西,我们称之为文学笔记。当你做文学笔记的时候,问自己这样的问题:
关于这篇笔记,有趣的点是什么?
有什么相关内容值得记录下来?
最后,为你的文学笔记创建一些标签。你的标签可以作为参考,并在需要时帮助你找到这篇文学笔记。
创建永久笔记
这些笔记将永远伴随着你。你可以通过查阅自己的文学笔记,找出其中的亮点,然后问自己:“从我读的材料中,我得到了什么样的领悟?”
答案需要你好好思考,但这正是为什么Zettelkasten如此有价值的原因。
与文学笔记相比,永久笔记是书面白话文。阅读你的永久笔记的人应该能理解它,而不需要阅读那些导致你想法的原始资料。
用Ahren的话说:
为每个想法写一个注释,就像你在为别人写一样。要使用完整的句子,著名资料的来源以做参考,并尽量做到精确,清晰和简短。
事实上,这是最重要的一步。一个笔记的价值取决于它的上下文,取决于与其他信息的联系网络。
你可以使用数字工具的双向链接功能,来链接与你的想法相关的永久笔记(当然,在一开始,你不能链接太多)。问自己这样的问题:
这个想法如何与我已经知道的内容相契合?
我如何用这个概念来解释Y?
X对Y意味着什么?
把一个笔记链接到另一个笔记是Zettelkasten方法的核心,也是它生发创意的关键。
“我们不花时间去好好吸收,不从我们读到的东西中学习,这才是真正的浪费时间。”
——snoke Ahrens
卡片盒笔记法 https://zettelkasten.de/introd…:
卡片盒笔记系统:英文/德语 Zettelkasten,将笔记相互连接形成一个网状结构的超文本笔记系统。
卡片盒笔记法:英文 Zettelkasten Method,建立卢曼笔记系统用到的的一些具体方法
笔记卡片:英文/德语 Zettel,原指一张用来记录笔记的小纸片,现指使用卢曼笔记法记录的一条单独的笔记
想法:英文 Thought,卢曼笔记法要求每条笔记只记录一个 thought,thought 在中文中含义非常丰富,可以是一个术语、概念、理论、看法、点子,突然的灵感等等,这些在本文中统称为“想法”
思想之网:英文 Web of Thought,由各种想法互相连接形成的网络,卢曼笔记系统就是一张思想之网
链接:英文 Link,名词,指代每条笔记的 ID
连接:英文 Link/Connect,动词,指将一条笔记与另一条笔记连接起来的动作/操作
卡片盒笔记系统是一款个性化的,用于思考和写作的工具。它具有超文本(hypertext)的特点,让你的所思所想互相连接形成网络。与其他系统不同的是,你创造的是一张由你的想法、看法、灵感、或者遇到的具体的知识组成的思想之网(web of thoughts),而不是孤立的笔记。它强调笔记之间的连接关系,而非把所有笔记堆在一起。
我们今天所知的卡片盒笔记法,由尼克拉斯-卢曼(Niklas Luhmann) 创造并发扬光大,它是目前最强大的思考和笔记工具。
尼克拉斯-卢曼是一位高产的社会学家,他出版了50本专著和600篇论文。他的生产力不是已发表的作品能够体现的,他的遗产中还有150份未完成的手稿,其中一份稿件的长度甚至超过1000页。
卢曼曾说,他的生产力源自于他与卡片盒笔记系统的合作。践行 卡片盒笔记法 的人对此一定很有共鸣。卢曼的卡片盒笔记系统是记有笔记的纸质小卡片的集合,但是这些卡片被做了一点点改变:它们是超文本。利用超文本的笔记,卢曼能够以合理的时间和精力浏览抽屉柜中的所有存放笔记卡片。但是,“合理”二字仅对卢曼而言,他痴迷于社会学理论,是一个精力十足的工作狂。
你看到一篇好文章,想要保存起来。这篇文章可能是一个网页,也可能是个邮件或者文件之类。
你把这篇文章直接全部扔到笔记本中存起来,想象某天会用到它,或者你会将里面的某个章节,某个图片单独保存起来。
如果超文本不是网页而是纸质的卡片,则需要付出更多的精力来跟随(follow)一个链接进入新的卡片。另外,你还需要一个入口,这个入口类似于维基百科的搜索框。卢曼创造了他的 Zettelkasten,并设计了进入卡片盒笔记系统的入口和一整套方法论,这让它他能够以一种高效的方式从一条笔记冲浪到另一条笔记。
即便如此,使用纸质的卡片盒笔记系统也是比较困难和耗费精力的(想象一下你要从几万张卡片里找到你需要的某些卡片)。我们比卢曼幸运,可以使用强大的数字工具。因此,我们不需要是卢曼一样的工作狂,也能从卡片盒笔记法中获益。
作者使用Org mode管理了他人生中的方方面面,比如写作系统、待办事项提醒、笔记系统等。得益于Emacs强大的定制开发能力,几乎你的一切需求都可以通过编写一些函数去扩展,这种扩展能力比VSCode/Vim/Sublime Text的插件系统等要强大的多。可以说,除了学习难度高,几乎没啥缺点了。它有以下特点:
17 years old。在时间的长河中,Org mode已经证明了自己。
定制能力强大。通过eLisp去开发,几乎无所不能。
纯文本。可自动生成多种格式的文件,也很容易发布成网页。
使用门槛很高。需要花费很多时间学习适应,这个“很多时间”可能比你想象的都要多一些。
生态差。由于高门槛,所以用户就少一些,生态就差一些。当然你能力强,可以用它来实现各种功能,但是这样挺累的。
技术迁移度差。eLisp太古老了,Emacs太独特了,这种上古技术,就像一把绝世好剑,注定能用好它的人会很少。这意味着你写出来的东西很多人都看不懂,当然也就无法进行技术交流互动了,自己造轮子造到心累。
新方案应该满足以下需求:
有强大的抓取能力。OneNote的这点很不错,你可以通过OneNote的浏览器插件很容易将外部文章存至OneNote的分区中,也可以将微信公众号文章发送给OneNote公众号进行保存。
有好的Tag系统。可以将分散的知识串联起来。虽然OneNote通过分页这种树形的方式组织相关的知识,但是资料一旦多了,分页真的很不方便,会让找资料变的很痛苦。
能容易的更新tag/link/text。如果重命名相关的资料名或者Tag,应该很方便去自动重命名关联的链接。
文件格式最好是纯文本的。如果不是纯文本,至少格式也不是专有的才好。
有强大的扩展能力。能通过插件去扩展定制。
开源。这点可以让我们放心的去使用,不太可能无人维护。
一个好的笔记系统,应该能把你所学习到的任何资料串联起来,形成一个知识系统,在你需要他们的时候,可以很容易找到,进而形成自己新的知识。
我在《用OneNote管理你的知识》这篇文章中介绍了如何用OneNote管理各种资料,虽然OneNote已经做的非常好了,依然存在以下问题无法达到我的要求:
排版不支持语法高亮。作为一个经常写代码的人,这点不能忍。
没有Tag系统。无法很灵活的给页面插入各种Tag ,导致资料无法有效互联,形成一个个知识孤岛,最终变成一个资料备份工具。
数据格式专有。只能用OneNote才能打开笔记本,假如微软突然放弃了这个工具,那就很尴尬了。
体积庞大。在使用了近5年之后,我的OneNote已经有几GB大小了,每次换电脑同步总是很慢,这可能和我的使用习惯有关系,遇到好的资料都是复制进去。
扩展能力差。无法安装插件,也没有定制能力,更无法将你的笔记发布成网站,也很难与别人分享你的知识。
go env -w GO111MODULE=‘on’
go env -w GOSUMDB=sum.golang.google.cn
go env -w GOPROXY=https://goproxy.cn,direct
当涉及到 Java 性能分析时,有一系列强大的命令和工具可以帮助开发人员分析应用程序的性能瓶颈、内存使用情况和线程问题。以下是一些常用的 Java 性能分析命令和工具,以及它们的详细说明和示例。
以下是一些常用的性能分析命令和工具汇总:
命令 功能简述
jps 用于查看正在运行的 Java 进程的状态信息。
jinfo 用于查看和修改正在运行的 Java 进程的 Java 虚拟机(JVM)参数和系统属性。
jstack 用于生成 Java 进程的线程转储信息。
jmap 用于生成 Java 进程的内存映射信息。
jconsole 是 Java 自带的监控和管理控制台,它提供了一个图形化界面,允许您监视和管理正在运行的 Java 应用程序的性能和资源使用情况。
jvisualvm 是一款功能强大的多合一故障诊断和性能监控的图形化工具。
1、jps
jps (JVM Process Status Tool)命令用于查看正在运行的 Java 进程的状态信息,包括进程 ID 和主类名。这对于迅速了解系统上正在运行的 Java 应用程序很有帮助。
1.1 语法
jps [ options ] [ hostid ]
其中,options 是可选的命令行选项,hostid 是可选的 RMI 注册表主机 ID。下面是一些常用的选项:
-q:只输出进程ID,不包括类名和JAR文件名。
-m:输出传递给 main 方法的参数。
-l:输出主类全名,或者对于JAR文件,输出JAR文件路径。
-v:输出传递给 JVM 的参数。
1.2 示例
假设您有一个正在运行的Java应用程序,其主类为com.example.MyApp,并且它的进程ID为12345。下面是一些使用jps命令的示例:
查看所有 Java 进程的进程 ID 和主类名:
$ jps
12345 com.example.MyApp
67890 sun.tools.jps.Jps
在上面的输出中,12345 是您的Java应用程序的进程 ID,com.example.MyApp 是应用程序的主类名。
只输出 Java 进程的进程 ID:
$ jps -q
12345
67890
输出 Java 进程的进程 ID 和传递给 main 方法的参数:
$ jps -m
12345 com.example.MyApp arg1 arg2
67890 sun.tools.jps.Jps -m
输出 Java 进程的进程 ID 和主类全名:
$ jps -l
12345 com.example.MyApp
67890 sun.tools.jps.Jps
通过使用不同的选项,您可以根据需要获取不同层次的信息。jps 命令对于快速查看正在运行的Java 进程非常有用,尤其在定位和监视Java应用程序时。
2、jinfo
jinfo 是一个 Java 命令行工具,用于查看和修改正在运行的 Java 进程的 Java 虚拟机(JVM)参数和系统属性。通过使用 jinfo,您可以动态地检查和更改 Java 应用程序的配置信息,而无需停止应用程序。
在很多情况下,Java 应用程序不会指定所有的 JVM 参数,开发人员可能不知道某一个具体 JVM 参数的默认值。在这种情况下,jinfo 就能很方便地查看 JVM 参数的当前值。
2.1 语法
jinfo [options] pid
其中,options 可以是以下选项之一:
-flags:打印指定 JVM 的参数值。
-sysprops:打印Java虚拟机的系统属性。
-flag name:打印指定名称的标志参数的值。
-flag [+|-]name:打印或设置指定名称的布尔标志参数的值。
pid 是正在运行的Java进程的进程ID。
2.2 示例
当使用jinfo命令来查看Java进程的信息时,可以得到类似下面的输出示例。以下是一些可能的输出示例,根据您运行的Java应用程序和虚拟机配置,实际输出可能会有所不同。
示例1:查看标志参数:
$ jinfo -flags 12345
Attaching to process ID 12345, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.12+7
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
Command line: -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
VM Flags:
-XX:CICompilerCount=3
-XX:InitialHeapSize=268435456
-XX:MaxHeapSize=4294967296
-XX:MaxNewSize=1431306240
-XX:MinHeapDeltaBytes=524288
-XX:NewSize=89128960
-XX:OldSize=179306496
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseFastUnorderedTimeStamps
-XX:+UseParallelGC
示例 2:查看系统属性:
$ jinfo -sysprops 12345
Attaching to process ID 12345, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.12+7
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
Command line: -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
Property settings:
awt.toolkit = sun.awt.windows.WToolkit
java.class.path = /path/to/your/application.jar
…
示例 3:查看特定标志参数的值:
$ jinfo -flag UseG1GC 12345
Attaching to process ID 12345, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.12+7
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
Command line: -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
UseG1GC=true
示例 4:修改特定标志参数的值:
$ jinfo -flag +PrintGCDetails 12345
Attaching to process ID 12345, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.12+7
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=268435456
3、jstack
jstack(Java Stack Trace)命令用于生成 Java 进程的线程转储信息。它可以显示每个线程的堆栈跟踪,帮助您找出可能的死锁、死循环和线程问题。
3.1 语法
jstack [ options ] pid
其中,options 是命令选项,pid 是目标 Java 进程的进程 ID。
以下是一些常用的命令选项:
-F:强制生成线程堆栈,即使Java进程没有响应。
-m:除了线程堆栈,还包括每个线程的本地(本地方法)信息。
-l:除了线程堆栈,还包括锁信息。
-h:显示帮助信息。
3.2 示例
使用 jstack 命令分析线程问题。
假设您有一个 Java 应用程序,出现了死锁的情况。您可以使用 jstack 命令来分析线程问题。以下是一个示例:
首先,找到目标 Java 进程的进程 ID(pid),可以使用 jps 命令来查看正在运行的Java进程:
jps
假设您找到了要分析的 Java 进程的进程 ID 为 12345,使用 jstack 命令来生成线程堆栈信息:
jstack 12345
命令会输出每个线程的调用栈信息,您可以在输出中查找线程状态、锁信息等。
假设您的 Java 应用程序中有两个线程,一个正在等待锁,而另一个持有该锁。这可能导致死锁。通过运行 jstack 命令,您可以获得类似以下的输出:
“Thread-1” 10 prio=5 os_prio=0 tid=0x00007f36e8001000 nid=0x5303 waiting for monitor entry [0x00007f36e3100000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.MyClass.method1(MyClass.java:50)
- waiting to lock <0x00000000e3033c88> (a java.lang.Object)
at com.example.MyClass.run(MyClass.java:100)
“Thread-2” 11 prio=5 os_prio=0 tid=0x00007f36e8001800 nid=0x5304 waiting on condition [0x00007f36e300f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.example.MyClass.method2(MyClass.java:75)
at com.example.MyClass.run(MyClass.java:120)
在这个示例中,Thread-1 正在等待获取一个锁,而 Thread-2 在持有锁的线程中等待。这可能是一个死锁的迹象,您可以根据这些信息来分析并解决线程问题。
总之,jstack 命令是一个有用的工具,用于生成Java进程的线程堆栈跟踪信息,帮助您分析和解决应用程序中的线程问题。
4、jmap
jmap(Java Memory Map)命令用于生成 Java 进程的内存映射信息。它提供了堆的详细信息,包括 Java 堆内存、对象统计和内存使用情况的详细信息,可以帮助开发人员分析内存泄漏、内存使用情况等问题。
4.1 语法
jmap [options]
其中,options 是一些可选的命令选项,
常用的 jmap 命令选项包括:
-heap: 显示Java堆内存使用情况。
-histo: 显示Java堆内存中的对象统计信息。
-dump:
-finalizerinfo: 显示等待终结者队列中的对象。
-F: 在无法连接到进程时,强制执行转储操作。
4.2 示例
示例1:使用jmap命令分析 Java 堆内存
假设您有一个 Java 应用程序正在运行,您想要分析其 Java 堆内存使用情况,以便查找内存泄漏问题。您可以使用以下命令:
jmap -heap
其中,
示例2:使用jmap命令生成堆内存转储文件
假设您怀疑 Java 应用程序存在内存泄漏,您可以使用 jmap 命令生成堆内存转储文件,以便后续分析。以下是一个示例:
jmap -dump:format=b,file=heapdump.bin
在这个示例中,format=b 表示将转储以二进制格式保存,file=heapdump.bin 指定转储文件名。您可以将
注意事项:
使用jmap命令时,建议在测试或开发环境中进行,避免在生产环境中使用,因为生成堆内存转储文件可能会影响应用程序的性能。
jmap命令可能需要JVM的调试权限,因此确保您有足够的权限来运行该命令。
转储文件可能会相当大,特别是在内存使用量较大的情况下。确保您有足够的磁盘空间来存储转储文件。
总之,jmap 命令是一个有用的工具,可以帮助开发人员分析 Java 应用程序的内存使用情况,查找内存泄漏问题,并生成堆内存转储文件以进行后续分析。
5、jconsole
jconsole(Java Monitoring and Management Console)是 Java 自带的监控和管理控制台,它提供了一个图形化界面,允许您监视和管理正在运行的 Java 应用程序的性能和资源使用情况。jconsole 是一个非常有用的工具,可以帮助开发人员识别问题、进行性能调优和监控Java应用程序。
5.1 主要功能
实时监控: 可以实时监控 Java 应用程序的内存使用情况、线程状态、垃圾回收、类加载等。
堆内存分析: 提供了对堆内存的监控和分析,可以查看对象数量、内存占用等信息。
线程分析: 可以帮助您检查线程的状态、堆栈跟踪和 CPU 使用情况,帮助您发现死锁和性能问题。
垃圾回收分析: 提供了垃圾回收的详细信息,可以查看垃圾回收的频率和效果。
MBean管理: 可以连接到 MBean(管理bean),允许您管理和监控应用程序的 MBean。
5.2 示例
以下是使用 jconsole 的示例,以监控一个正在运行的 Java 应用程序:
启动您的 Java 应用程序。假设您的应用程序是一个简单的 Java 程序,例如:
public class MyApp {
public static void main(String[] args) {
while (true) {
System.out.println(“Running…”);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
打开终端并运行以下命令,启动 jconsole:
jconsole
在 jconsole 界面中,您可以看到一个列表,显示了正在运行的 Java 进程。选择您的应用程序进程并点击“连接”。
jconsole:新建连接
在 jconsole 的不同选项卡中,您可以查看内存使用情况、线程状态、垃圾回收信息等。例如,您可以在“内存”选项卡中查看堆内存使用情况。
jconsole:内存
在“线程”选项卡中,您可以查看每个线程的状态、堆栈跟踪等信息,以帮助您识别潜在的线程问题。
jconsole:线程
jconsole 是一个强大的 Java 性能监控和管理工具,提供了丰富的功能来监视和分析应用程序的性能和资源使用情况。通过 jconsole,开发人员可以轻松地识别问题、分析性能瓶颈,以及进行调优,从而提升Java应用程序的性能和效率。
6、VisualVM
Java VisualVM(Java Visual Monitoring and Troubleshooting Tool)是一款功能强大的多合一故障诊断和性能监控的图形化工具,它集成了多种性能统计工具的功能,使用它可以代替 jmap、jstack 甚至 jconsole。
它是 Java 开发工具包(JDK)的一部分,可以帮助您实时监控应用程序的各种指标,如内存使用、线程情况、垃圾回收等,以便发现和解决性能问题。
6.1 主要功能
实时监控: 允许您实时监控Java应用程序的运行状态。您可以查看内存使用情况、线程状态、CPU利用率等指标。
内存分析: 提供了内存分析工具,可以帮助您识别内存泄漏和对象分配情况。您可以查看堆内存的内容,分析对象引用关系。
线程分析: 可以监控和分析应用程序中的线程状态,帮助您发现死锁、线程争用等问题。
垃圾回收分析: 工具可以显示垃圾回收的详细信息,帮助您确定垃圾回收的类型、频率和影响。
CPU分析: 可以帮助您分析 CPU 利用率高的原因,找出耗费 CPU 资源的部分。
多种插件: 支持各种插件,扩展了其功能。您可以安装插件来支持不同的 Java 应用程序和特定的性能分析需求。
6.2 示例
监控内存和 CPU 使用。
启动 Java VisualVM: 您可以在JDK的 bin 目录中找到jvisualvm.exe(Windows)或 jvisualvm(Linux/macOS)并运行它。
jvisualvm
连接到应用程序: 在 Java VisualVM 中,点击的“远程”按钮,然后在“远程”面板中添加要连接的远程或本地Java进程。
监控性能: 连接到应用程序后,您可以查看实时性能监控信息,包括内存、线程、CPU使用等。您还可以选择不同的监视选项,如“监视”、“内存”、“线程”等标签。
jvisualvm:监控
进行内存分析: 在“抽样器”标签中,您可以生成并分析堆转储,查看对象引用关系、占用内存的对象等。
jvisualvm:抽样器
线程分析和垃圾回收分析: 在“线程”和“监视”标签中,您可以监控线程状态、识别死锁,以及查看垃圾回收行为。
jvisualvm:线程
总之,Java VisualVM 是一个强大的性能分析工具,可以帮助您监控、分析和优化Java应用程序的性能。通过实时监控、内存分析、线程分析等功能,您可以更深入地了解应用程序的运行情况,并解决性能问题。
7、总结
Java 性能分析是确保应用程序优化和效率的关键步骤。这些命令和工具提供了详细的信息,可以帮助您分析和解决性能问题。无论您需要查找内存泄漏、分析线程问题,还是优化垃圾回收,这些工具都能够提供有价值的信息。掌握这些命令和工具,将有助于您更好地管理和优化您的Java应用程序,提供更好的用户体验。
引言
Java应用程序的开发和部署过程中,合理利用各类环境变量是关键之一。不同类型的环境变量,如系统环境变量、进程级环境变量、Java启动参数设置的系统属性以及Spring Boot配置文件中的环境变量,它们之间存在优先级差异。
深入理解这些环境变量的优先级关系,将使得Java应用程序更加灵活和易于维护。本文将详细介绍这些环境变量的优先级,结合实际场景提供最佳实践,以帮助开发人员更好地配置和管理Java应用程序。
示例: 假设在操作系统中设置了一个名为 JAVA_HOME 的系统环境变量,指向Java的安装目录。尽管Java应用程序继承了这个变量,但它并不直接用于应用程序的特定配置。
2.2 进程级环境变量
进程级环境变量是在Java进程启动时设置的,可在启动脚本或命令行中指定。这一层级的环境变量会覆盖系统环境变量,但只在Java进程的生命周期内有效。进程级环境变量的优势在于可以根据应用程序的不同运行环境进行定制化配置。
示例:
通过启动脚本或命令行参数 -D 设置Java进程的环境变量,例如:
java -Dspring.profiles.active=dev -jar myapp.jar
这里的 spring.profiles.active 是一个进程级环境变量,用于指定Spring应用程序的激活配置文件。
2.3 Java启动参数 (-D参数)
通过 -D 参数可以在启动Java进程时设置Java系统属性。这些属性可以通过 System.getProperty() 方法在Java应用程序中获取。Java启动参数设置的系统属性优先级较高,通常用于指定应用程序的一些关键配置。
示例:
java -Dserver.port=8080 -jar myapp.jar
在这个例子中,server.port 是一个Java系统属性,它会覆盖进程级环境变量和系统环境变量中的同名属性。
2.4 Spring Boot配置文件中的环境变量
在Spring Boot应用程序的配置文件中,可以使用 ${} 语法引用环境变量。这样的环境变量可以是系统环境变量、进程级环境变量,甚至是Java启动参数设置的系统属性。Spring Boot配置文件中的环境变量通常用于配置应用程序的各种属性,如数据库连接、端口号等。
示例:
server:
port: ${SERVER_PORT:8080}
在这个例子中,如果环境变量 SERVER_PORT 存在,则使用该值,否则默认使用 8080。
Java启动参数设置的系统属性(-D参数):优先级最高。
进程级环境变量:次高优先级。
系统环境变量:优先级最低。
Spring Boot配置文件中的环境变量:介于进程级环境变量和系统环境变量之间,可通过动态设置或默认值实现更灵活的配置。
4. 最佳实践
动态切换配置文件:通过Java启动参数设置系统属性,可以实现在不同环境中动态切换配置文件,提高灵活性。
保护敏感信息:避免将敏感信息硬编码在代码中,通过环境变量传递,可以在不同环境中轻松更改这些信息。
日志级别控制:使用Java启动参数设置系统属性,可以在运行时动态调整日志级别,有助于排查问题。
多环境配置:利用Spring Boot配置文件中的环境变量,可以为不同环境提供特定的配置,如数据库连接信息、服务端口等。
. Golang 多架构编译的原理
Golang 的多架构编译功能依赖于两个关键的环境变量:GOOS 和 GOARCH。其中,GOOS 表示目标操作系统,而 GOARCH 表示目标架构。通过设置这两个环境变量,我们可以告诉 Go 编译器在编译过程中要生成的目标平台。
编写代码:准备好你的 Go 代码,例如一个简单的 main.go 文件,其中包含了你的应用程序逻辑。
进行编译:使用 go build 命令进行编译,同时指定目标操作系统和架构。例如,要在 CentOS amd64 上为 Linux 64 位编译,可以使用命令 GOOS=linux GOARCH=amd64 go build -o hello-linux-amd64 main.go。
验证编译结果:编译完成后,可以验证生成的可执行文件是否能够在目标平台上正常运行。
package main
import “fmt”
func main() {
fmt.Println(“Hello, World!”)
}
使用以下命令,我们可以将这个简单的 Go 程序编译为多个目标平台的可执行文件:
GOOS=linux GOARCH=amd64 go build -o hello-linux-amd64 main.go
GOOS=windows GOARCH=amd64 go build -o hello-windows-amd64.exe main.go
GOOS=linux GOARCH=arm64 go build -o hello-linux-arm64 main.go
GOOS=darwin GOARCH=amd64 go build -o hello-darwin-amd64 main.go
通过以上命令,我们可以在不同的操作系统和架构上编译出相应的可执行文件,使得我们的应用程序能够在各种环境中运行。
3.2 编写多架构编译脚本
为了方便编译多个架构,可以编写一个脚本来自动编译多个目标平台。
创建 build.sh 脚本:
#!/bin/bash
OUTPUT_DIR=“build”
PLATFORMS=(“linux/amd64” “linux/arm64” “windows/amd64” “darwin/amd64”)
mkdir -p $OUTPUT_DIR
for PLATFORM in “\({PLATFORMS[@]}"; do OS=\) (echo \(PLATFORM | cut -d'/' -f1) ARCH=\) (echo \(PLATFORM | cut -d'/' -f2) OUTPUT_NAME=\) OUTPUT_DIR/hello-\(OS-\) ARCH
if [ $OS = "windows" ]; then
OUTPUT_NAME+='.exe'
fi
echo "Building for $OS/$ARCH..."
GOOS=$OS GOARCH=$ARCH go build -o $OUTPUT_NAME main.go
done
使脚本可执行:
chmod +x build.sh
运行脚本:
./build.sh
这个脚本会在 build 目录中生成多个目标平台的可执行文件。
3.3 安装和使用交叉编译工具链
对于一些架构,例如 ARM,你可能需要安装特定的交叉编译工具链。例如:在 CentOS X86 架构的环境上,编译 ARM 架构的应用。
目前,CentOS 官方仓库不直接提供 gcc-arm-linux-gnu 和 gcc-aarch64-linux-gnu 包。因此,我们可以从开发者网站(如 ARM 官方或 Linaro)下载预编译的工具链。下面示例展示如何安装 Linaro 提供的 ARM 工具链。
下载 Linaro 64 位 ARM 工具链
wget https://releases.linaro.org/co…
解压并安装工具链
sudo tar -C /usr/local -xvf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz
将工具链添加到系统环境变量
echo “export PATH=$PATH:/usr/local/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin” » ~/.bashrc
source ~/.bashrc
验证工具链安装
安装完成后,可以通过以下命令验证工具链是否安装成功。
aarch64-linux-gnu-gcc –version
使用 ARM 工具链进行交叉编译
安装完成并验证工具链后,你可以使用这些工具链为 ARM 平台进行交叉编译。例如:
GOARCH=arm64 GOOS=linux CC=aarch64-linux-gnu-gcc go build -o hello-arm64 main.go
通过这些步骤,你可以在 CentOS 7 amd64 上安装 ARM 工具链,并为 ARM 平台进行交叉编译。
3.4 使用 Docker 进行多架构编译
Docker 提供了一种方便的方法来进行多平台构建。你可以使用 Docker 的 Buildx 插件来构建不同平台的 Docker 镜像。
安装 Docker 和 Buildx
sudo yum install -y yum-utils
sudo yum-config-manager –add-repo https://download.docker.com/li…
sudo yum install docker-ce docker-ce-cli containerd.io
sudo systemctl start docker
sudo systemctl enable docker
docker run –rm –privileged multiarch/qemu-user-static –reset -p yes
docker buildx create –use
创建 Dockerfile
创建一个简单的 Dockerfile:
FROM golang:1.18 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /hello
FROM alpine:latest
COPY –from=builder /hello /hello
CMD ["/hello”]
构建多平台 Docker 镜像
docker buildx build –platform linux/amd64,linux/arm64 -t hello:latest –push .
4. 结语
通过这篇文章,读者可以全面了解 Golang 的多架构编译功能,掌握如何使用这一功能来实现跨平台部署。同时,通过最佳实践示例,读者可以更加直观地理解 Golang 多架构编译的实际应用场景,为日后的开发工作提供参考和指导。
Orange Pi AI Pro 开发板是香橙派联合华为精心打造的高性能 AI 开发板,其搭载了昇腾 AI 处理器,可提供 8TOPS INT8 的计算能力,内存提供了 8GB 和 16GB 两种版本。可以实现图像、视频等多种数据分析与推理计算,可广泛用于教育、机 器人、无人机等场景。
开发板使用总结
易用性
官方文档很详细,容易上手。配置简单,安装方便。开发板接口很丰富,支持很多常见接口,如千兆网口、USB、Type-C、WiFi、蓝牙等,属于开箱即可用的开发板。
负载能力
OrangePi 的性能虽然不及高端服务器,但对于家庭 NAS 用途足够胜任。使用 Nextcloud 进行文件存储和小规模的文件共享是完全没问题的。在进行文件上传、下载以及媒体流播放时,OrangePi 的 CPU 利用率会有所上升,但仍能保持较为流畅的操作体验。
散热能力
OrangePi 开发板具有非常高的散热能力,可以满足大多数家庭 NAS 的需求。在长时间运行下,OrangePi 的温度会略有上升,尤其是在进行大文件传输或高负载操作时。建议使用更大的散热片或风扇进行辅助散热,以确保系统稳定运行。
噪音能力
OrangePi 本身没有活动部件,因此在正常运行时是完全静音的。如果使用外部风扇,噪音水平取决于风扇的质量和速度。一般来说,小型风扇的噪音是可以接受的,不会对日常生活造成明显影响。
6.2 个人云盘使用总结
在实际使用中,OrangePi 搭建的 NAS 系统表现令人满意。以下是一些具体的使用感受:
文件访问速度
在局域网内访问文件速度较快,上传下载速度基本能够满足日常需求。使用Nextcloud客户端可以方便地同步文件。
多设备支持
Nextcloud支持多种设备访问,包括Windows、Mac、Linux、Android和iOS设备。可以随时随地访问你的文件,非常方便。
数据安全性
可以通过设置用户权限和加密来保护你的数据安全。定期备份也很重要,可以使用外部硬盘或其他NAS设备进行备份。
拓展性
Nextcloud支持多种插件和应用,可以根据需要扩展功能,如日历、联系人管理、在线文档编辑等。
一、NFS简介
NFS(Network File System) 是一种分布式文件系统协议,允许用户通过网络在不同主机间共享文件和目录。它适用于局域网环境,常用于服务器集群、数据共享等场景。本文详细介绍NFS服务端与客户端的安装、配置及挂载流程。
二、NFS服务端配置
Ubuntu/Debian
sudo apt update && sudo apt install nfs-kernel-server
CentOS/RHEL
sudo yum install nfs-utils # CentOS 7
sudo dnf install nfs-utils # CentOS 8+/RHEL
2. 创建共享目录
sudo mkdir -p /mnt/nfs_share
sudo chown nobody:nogroup /mnt/nfs_share # 设置权限(根据需要调整)
3. 配置NFS导出规则
编辑配置文件 /etc/exports,定义共享目录及访问权限:
/mnt/nfs_share 192.168.1.0/24(rw,sync,no_subtree_check)
常用权限选项:
rw:读写权限。
ro:只读权限。
sync:同步写入磁盘(数据安全性高)。
async:异步写入(性能更好,但可能丢失数据)。
no_root_squash:允许客户端root用户保留权限(谨慎使用)。
示例:
/mnt/nfs_share *(rw,sync,no_root_squash)
4. 生效配置并启动服务
sudo exportfs -a # 重新加载 exports 配置
sudo systemctl start nfs-server # 启动服务
sudo systemctl enable nfs-server # 设置开机自启
5. 防火墙配置
开放 NFS 相关端口(NFSv4 默认使用 TCP 2049):
sudo ufw allow 2049/tcp # Ubuntu
sudo firewall-cmd –permanent –add-service=nfs && firewall-cmd –reload # CentOS
三、NFS客户端配置
sudo apt install nfs-common
CentOS/RHEL
sudo yum install nfs-utils
2. 创建本地挂载点
sudo mkdir -p /mnt/nfs_client
3. 手动挂载NFS共享
sudo mount -t nfs <服务端IP>:/mnt/nfs_share /mnt/nfs_client
示例:
sudo mount -t nfs 192.168.1.100:/mnt/nfs_share /mnt/nfs_client
4. 自动挂载(重启生效)
编辑 /etc/fstab 文件,添加以下行:
<服务端IP>:/mnt/nfs_share /mnt/nfs_client nfs defaults 0 0
示例:
192.168.1.100:/mnt/nfs_share /mnt/nfs_client nfs defaults 0 0
5. 验证挂载
df -h | grep nfs # 查看挂载状态
touch /mnt/nfs_client/test.txt # 测试读写权限
四、高级配置与调试
sudo mount -t nfs -o vers=4 192.168.1.100:/mnt/nfs_share /mnt/nfs_client
2. 用户身份映射
在服务端 /etc/exports 中配置 anonuid 和 anongid:
/mnt/nfs_share 192.168.1.0/24(rw,sync,all_squash,anonuid=1000,anongid=1000)
3. 查看NFS共享状态
服务端:
showmount -e localhost # 查看已导出的共享目录
客户端:
showmount -e <服务端IP>
4. 日志排查
服务端日志:/var/log/syslog(Ubuntu)或 /var/log/messages(CentOS)。
客户端日志:dmesg | grep nfs。
五、常见问题解决
权限被拒绝(Permission Denied)
检查服务端 /etc/exports 的权限配置。
确保客户端用户对挂载点有访问权限。
连接超时或无法访问
确认防火墙已放行NFS端口(尤其是NFSv3需要额外开放 rpcbind 端口)。
使用 rpcinfo -p <服务端IP> 检查RPC服务状态。
挂载后文件属主显示为nobody
在服务端配置 no_all_squash 或指定 anonuid/anongid。
六、总结
NFS 是 Linux 环境中高效的文件共享解决方案。通过配置服务端导出规则、客户端挂载目录,并合理设置权限与防火墙规则,即可实现稳定可靠的网络存储。对于生产环境,建议结合Kerberos增强安全性,并定期监控NFS性能。
注意:NFS默认不加密传输数据,公网环境建议使用VPN或SSH隧道保护通信。
JDK 17 是继 JDK 8 后的首个 LTS 版本,支持模块化、Records 等新特性,但移除了部分旧 API(如 javax.servlet)。
直接影响:基于 JDK 8 构建的 SpringFox(Swagger 2.x)因依赖旧规范无法兼容新版本。
Jakarta EE 的崛起:
Java EE 移交 Eclipse 基金会后更名为 Jakarta EE,包名从 javax.* 改为 jakarta.*。
核心冲突:Spring Boot 3.x 和 SpringDoc(Swagger 3.x)强制依赖 Jakarta EE 9+,导致旧项目升级时需全局替换包名。
安全风险:SpringFox 已停止维护,存在未修复漏洞(如 CVE-2021-28170)。
功能需求:SpringDoc 支持 OpenAPI 3.0 规范,提供更灵活的文档定义和响应示例。
生态兼容:微服务、云原生场景下,JDK 17 的容器化支持更优。
微服务的概念,最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出
Martin Fowlern 在《Microservices》一文是如:
Martin Fowler
In short, the microservice architectural style is an approach todeveloping a single application as a suite of small services, each running inits own process and communicating with lightweight mechanisms, often an HTTPresource API. These services are built around business capabilities andindependently deployable by fully automated deployment machinery. There is abare minimum of centralized management of these services, which may be writtenin different programming languages and use different data storage technologies.
如 Martin 所言,将单体应用拆分为一组微小的服务,每个微小的服务单独运行,服务间可通过如 RESTful API 这样轻量级的接口交互,这些服务以业务能力为核心,用自动化部署机制独立部署。这些服务可以用不同语言开发,用不同技术来存储数据。
以我理解来看,微服务架构的特性如下:
将单体应用进行解耦,按照一定方式(如:业务分类等)拆分为多个微小的服务,微服务间相互交互以完成实际业务流。
微服务间通信方式更轻量化,如:RESTful。
各微服务支持单独部署、单独运行。
各微服务的开发语言不限,可交叉选择不同语言。
简单来说,微服务其实是从早期的 CORBA、COM+ 等技术,到后来的 SOA、RESTful 架构,是一种分布式计算思想的延续。
具体来说,把单体应用拆分为一个一个微小服务,而这些微小服务又不依赖任何服务器,使其可以通过自动化方式独立部署,每个服务可以运行在自己的进程或 Docker 容器中,通过轻量的通信机制,能够基于业务能力快速构建,动态扩容,实现集中化管理的系统架构。
为什么要使用微服务
伴随着互联网系统的爆发、系统的多样化以及系统分层切块的演变,系统变得越来越复杂,调用链也越来越复杂,传统单体系统已经无法再支撑这种变化,因此微服务的思想也就顺应而来,用来解决这种现状。
传统的单体系统,企业往往需要耗费几个月乃至几年,才能落地一个系统,达到上线的标准。这就给一些小公司的前进带来了瓶颈,没人敢轻易研发、重构一个新的产品,但在现在互联网日益变革的时代,不得不大胆向前尝试,力争在最短的时间内完成一个新的产品。在互联网时代常常要求一周内完成一个功能或小项目,这种不断伸缩的业务形态,不断要求缩短的开发周期,使得我们需要在系统的扩展、伸缩、减低相互影响上做出文章。
那么,怎么才能达成系统的扩张呢?在 Microservice Architecture 一文中提到,我们需要将服务进行拆分,拆分为前置服务和业务服务,并在前端新增 SLB(Server Load Balance),用一组相同的前置服务组成及其来提供服务。
而减低各模块、各业务的相互影响,就需要将单体系统按照模块或业务进行拆分,以此来减低其耦合性。
上面提及到问题,在微服务架构下,给出了一些完美的解决方案。
1.模块服务化
单体系统,团队在多人协作开发时,往往会存在因代码、设计思路等差异而造成相互影响,相互等待对方的现状,而且系统的庞大也给后期维护带来诸多不便。而微服务最突出的一个特性“解耦”,恰恰解决了这种问题,让系统更加轻量化,便于多人协同开发而互不依赖。
2.独立部署,灵活扩展
传统的单体架构是以整个系统为单位进行部署,而微服务则是以每一个独立服务(例如:用户服务,文件服务等)为单位进行部署。用下图能够更好的体现:
独立部署,灵活扩展
左边是单体架构的集群,右边是微服务集群 。
各个服务都是独立部署,可以根据各自服务的特点进行适当调整,即:根据服务的吞吐量、压力等不同的指标,分别给出不同的部署方案(部署策略),使得资源更加充分合理的使用。这种灵活部署只有微服务架构才能实现。
3.资源的有效隔离
这是微服务设计的原则之一,就是每一个微服务拥有自己独立的数据源,假如微服务 A 想要读写微服务B的数据库,只能调用微服务 B 对外暴露的接口来完成。这样有效避免了服务之间争用数据库和缓存资源所带来的问题。
资源的有效隔离
如果采用 Docker 部署,则每一个微服务实例在 Docker 容器上运行,更加完美的实现了服务器资源(内存、CPU 资源等)的有效隔离 。
4.多语言,多选择
在微服务架构下,因为有了模板服务化,各模块互不依赖的特点,对开发语言的选择就没有统一的要求,完全可以根据企业技术人员情况,不同模块的特点来选择不同的开发语言,让开发变得更加多样化。
5.团队组织架构的灵活
微服务架构设计的思想,改变了原有的企业研发团队的组织架构。传统的研发组织架构是水平架构,前端有前端的团队,后端有后端的团队,DBA 有 DBA 的团队,测试有测试的团队。
而微服务架构的设计思想对团队的划分有了一定的影响,使得团队组织架构的划分更倾向于垂直架构,比如用户业务是一个团队来负责,支付业务是一个团队来负责。这种团队组织架构,也更好的协同来完成一个系统。
6.组件/框架多样化、成熟化
伴随着微服务出现,不断膨胀,各类技术组件、框架应用而生,为我们的开发降低了成本,加快了项目的开发周期。这些组件/框架纷纷落地投产,变得更加的稳定成熟。Spring Cloud 家族就是一类典型的代表,后续文章将在详细介绍在微服务中的技术选型。
正因为微服务上述这些特性,使得在微服务的影响下,各类项目顺势崛起,为各类中小型软件公司带来了希望。
杀手锏,tcpioneer#
影视飓风2024/10/9 补档:清晰度不如4年前!视频变糊是你的错觉吗?
磁力链接:magnet:?xt=urn:btih:ffb877b0e3e964cb307048542faa22571f515245
凯恩斯交叉
发布时间:2016年10月1日
对于西方经济学,学校开设了一个学期的课程,授课的孙老师上来就同我们讲:学时不够。
她认为这门课最好拆分成微观经济学和宏观经济学两个学期,才能讲的稍微那么完整入门一些。 接着还应当讲高级的内容,加入计量经济学和数学分析。无奈培养计划安排如此,她也没有办法。 在推荐材料上,老师给我们了推荐曼昆的《经济学原理》上下册,很厚的两本书。 我最怕大部头了,可是实际读来却发现,老师果然很懂我们的智商。 几堂课听下来,我只记住了那朗朗上口的 “比凯恩斯还凯恩斯的凯恩斯交叉”。 可读那本大部头,就好像有一个老头在旁边给我讲故事,娓娓道来,栩栩如生。
其实十年寒窗上来,读书也是有循序渐进的道理,一年级有一年级的教材,三年级有三年级的教材。 但是大学老师推荐的书就不一样了,往往是经典中的经典,或者是同自己相关的利益团体出版的内容。 这就容易导致什么问题呢?我们刚接触这个领域,不知道什么是好书,什么是烂书。 用心一点的老师,会在幻灯片推荐材料的那一页注明:哪些是适合初学者看的,哪些是适合到达一定阶段的人看的, 哪些是学者们还在研究的… 哪些是我们教学组编的,也是很不错的参考资源。嘿嘿,懂你意思。
一些科研型的老师,在写文章列参考文献的时候没有这样的需求,提到了就加上去。
最可怕的是有些推荐材料的第一书目就是一本高层次的书籍,初学者读起来晦涩难懂。 所以有些时候读书如爬山,学生境界不到,就会说一些书是烂书;境界起来了,又觉得写得有些道理;等自己独成一派了,又觉得写得很有局限性。 要看山是山,到看山不是山,最后看山还是山。不同的作者只是提供了不同的角度,哪能说高鸿业的书就一定是不如曼昆的呢?
阳春白雪,下里巴人,各有千秋罢了。
国内的教材喜欢写得精辟,国外的教材喜欢写得通俗。 前者作为教科书,需要老师带着解释;后者像一本故事书,学生自己读也能津津有味,故事书的篇幅当然会更多了! 这里也体现了老师和大师的差距,大师能够站在更高的视角,却用更质朴的语言去解释一件事情,可以提供不同的角度。 而普通学者尚未达到那样的境界,所以只能从自身专业的角度来谈问题,不考虑衔接的过程。 这可能就是常识水平差异,或者说知识的诅咒。
读者也要对自己有些了解,不然明明是自己读得太浅,反倒要说作者的不是,容易贻笑大方。 烂书绝对是有的,但是你不多读点书,怎么能在很快的时间内知道这是一本初学者书籍,还是高观点书籍,亦或者是彻彻底底的烂书呢? 一种广为流传的说法是:先多读书,再读好书,最后才是好读书。倘若不自己经历一番,怎么会明白其中的道理。
同学期授课其它课程的,还有《应用文写作》的林老师,《高等数学》的沈老师,以及《线性代数》的满老师等人。 满老师上第一堂课时很有趣,她也列出了一系列的推荐材料,并且点评了同济大学的教材和其它教材的区别。 她带着欢快的语气告诉我们:“同学们,线性代数很有意思的,线性代数的知识以后在很多领域都会被用到的。”
坐在下面的学生,包括我在内,都是刚从高中环境上来的,看上去一片呆气 —— 因为大家都尚不知道,推荐材料这一页,需要一个学者用多大的心血去整理。 可能只觉得正课的内容考试会考到,所以才是关键的吧。 满老师仿佛没有得到想要的回应,眼神变得有些失落,但又很快进入下一个话题,可能是已经习惯了。 我们默认用同济的教材上课。
他给我们推荐了很多书,其中一本叫作《乌合之众》。 学期末的时候,他给我们放了一部德国电影,叫作《浪潮》。 这两部作品之间其实存在着很大的关联,范老师告诉我们要保持思考和判断,不要轻易地相信某个人的话都是对的。 有一点逻辑悖论的感觉,范老师的话都是对的吗? 但范老师是上大学以来第一个直接对我(们)说这些话的人。
我很喜欢 Philip Guo 个人网站的风格,有 VLOG / PODCAST / ARTICLES 三种记录生活的方式,他的创作原则我很喜欢:
One of my main hobbies is writing articles on this website (and more recently, recording podcasts and vlogs). One heuristic I use when deciding what to write is to ask myself:
Will at least 100 people care about this topic three years from now?
The following don’t qualify, so I rarely write about them:
commentary on news events or fads
reactions to someone else’s article
personal notes that only my friends would care about
Finally, if I see many people writing about a topic, then I avoid it unless I have a drastically different perspective.
沈向洋博士的七个经验和教训
发布时间:2020年11月16日
内容距离首次发布已有 1683 天,请注意时效性。
全文摘录自,最近沈向洋博士在 X-Talk 上的演讲,
你不可能做所有的事情
在自己的专业一定要做得很深
一定要把故事讲好
一定要有一个远大的目标
Difficult, not Impossible
一个一个项目加起来
一定要明白自己前进的方向
你不可能做所有的事情
第一件事情,当你职业生涯刚刚起步的时候,一定要明白 你不可能做所有的事情,你可能有很多很多的想法,但是不见得什么事情都可以做到。就像我刚才讲的,我一直想做教授,但是我在最后一刻改变主意了,我一个朋友说服了我去参加他的 start-up. 他是这样说服我的,我跟他在车里谈了四个小时,他叫 Eric。他说:Harry,我最后终于明白了,你想当教授。他说:那简单,你先参加 start-up,先去赚很多很多钱,我给你的母校卡内基 · 梅隆捐一个 Harry Shum Professorship,指定你是第一个获奖人,这样的话你可以给学校再捐一个 Harry Shum Robotics Center. 当然这件事情没有发生。事实上我后来参加了这样一个 start-up 以后,很快又加入了微软。当时很重要的一件事情,就是第一个孩子出生了。当时我想了一下两件事情不可以同时成立的:做 start-up 和带一个小孩子。但是孩子生出来了以后,你就没有办法去 Get rid of(摆脱),所以你只能 Get rid of startup.
在自己的专业一定要做得很深
第二个 lesson,其实人一生的职业旅程非常长,首先我们职业生涯刚刚起步的时候,非常重要的一件事情就是 在自己的专业一定要做得很深,你一定要有一件事情、某一个方向,大家知道你做了点什么样的工作。我自己非常幸运参加微软,博士以后在微软研究院的时候,跟很多的同事,当时计算机视觉领域中非常优秀的工程师和科学家一起工作,包括待会儿上台的张正友博士,一起做了很多方面的工作。我自己做的一个方向叫做 Motion Estimation,特别当时做全景图,就是 Panoramic Image,拍了几张照片以后把它拼起来这样。我也很自豪地跟大家讲,今天大家用手机拍全景图的时候,说不定也用了我们的技术。这件事情很重要,特别是你刚刚起步的时候。很多人忘记了,如果你不在某一个方向做到足够深的话,大家就记不住你。
一定要把故事讲好
第三点我想重点讲一下,就算是对工程师和科学家来讲,除了你专业做得好以外,最重要的一件事情大家不要忘记,就是一定要把故事讲好。我一辈子听过很多了不起的演讲。很多很多年以前,在 SIGGRAPH 会议上我听过一个 keynote,是迪士尼的 VP of Imagination Engineering 做的演讲。为什么讲故事很重要?他说你在迪士尼看了那么多的电影、那么多的动画,不同的历史阶段,从二维动画到三维动画,到现在 VR 这些东西。这些实际上都不是最重要的事情,最重要的是大家喜欢迪士尼是因为迪士尼背后的这些故事。对我们很多科学家、工程师来讲,我们也经常要做一些报告。大家做报告时,可能里面写的字非常非常多。我自己很幸运在很年轻的时候,研究生阶段就有机会参加了 SIGGRAPH,1995 年。这是我好朋友 Eric Chan 的报告,这是我一辈子见过的技术 Presentation 里面得到掌声次数最多的。他当时在苹果公司,写了一篇文章叫 QuickTime VR,当时我在苹果公司做实习生。Eric 在台上,他有特别的 Style,整个十几分钟的演讲,一共有八次掌声,我记得我在台下看着 Eric 演讲的时候,觉得非常了不起。一个中国人讲一口台湾腔英语,能够让大家有八次掌声,这非常非常了不起。所以我就想在这里跟大家讲 story telling 非常重要,比如做 presentation,一张 slide 上面不应该超过七行字,多了以后大家看不清楚也搞不懂,这些东西都非常非常重要。
一定要有一个远大的目标
第四个 lesson,我想给大家讲的是一定要有目标,一定要清楚自己最后要追求什么。我自己非常荣幸,2004 年开始担任微软亚洲研究院院长,当时我们定下来的目标,就是说一定要成为世界上最了不起的研究院,后来我们也基本上达到这样一个目标。我记得 MIT Technology Review 曾经写过一篇封面报道叫《The World’s Hottest Computer Lab》。那篇文章出来时,我正好在美国出差。美国机场这些地方有专门卖杂志的,我当时不知道 MIT Technology Review 的文章会出来,封面上就是我两个同事的照片。然后我就看看那个卖杂志的女士,我就跟她讲「May I have all your copies?」那个女士很高兴,先收了我的钱,然后问我「May I ask why?」然后我就很自豪地跟她讲,我说「Look, those two people on the cover, they work for me.」所以你一定要有一个远大的目标,做了不起的事情。
Difficult, not Impossible
第五个 lesson,其实是我这么多年工作,特别是后来从研究部门到了产品部门以后,对处理所有问题的复杂性有了更深刻的认识以后,自己创造出来的一段话,叫做「Control the controllable, observe the observable, leave the rest alone」. 把握可控的,留心可见的,余下顺其自然。我本科的时候念的是自动控制专业,大家有一些控制理论背景的话,就会对这个句式很熟悉。因为当时我从研究院调出去以后,到微软的产品部门做搜索引擎,做 Bing。我们的工作当然就是跟谷歌竞争,跟谷歌竞争当然不是开玩笑的事情,大多数人都会觉得跟谷歌弄的话下场肯定是很悲惨的。但是我很自豪地跟大家讲,现在微软 Bing 这样一个业务线的话,一年也挣 100 亿美元,所以我们其实做得还是相当不错的,在美国超过三分之一的搜索量来自于 Bing。当时大家就在想:一个研究院来的人,他怎么可以去带产品线?所以我当时就在想,作为一个新兵到产品部门去工作,大家都问:你到这里来也没有做过产品,你可以给我们带来什么?我说我也不知道可以给大家带来什么,但是我想我至少可以跟大家保证,等我哪天离开这里的时候,大家会 remember me as the VP who knows how to party。所以你一定要想到,你自己有哪些地方可以去 motivate 大家,可以把大家团结起来。哪些东西你自己是可以 control 的,如果你不可以 control 的话,你召集了也没有用,你就应该去观察这样的一些事情。所以在这件事情上大家一定要想。这是我最喜欢的一张 slide,大家看过《教父》的话都会知道,Michael Corleone 经常讲,不管什么时候你遇到多大的困难,Difficult, not Impossible. Remember that.
一个一个项目加起来
第六个 lesson,我想跟大家分享的,实际上我自己非常有幸,当时和前同事 Jim Gray,这个你们可能还记得,1998 年图灵奖得主,以前是微软研究院的研究员,他非常非常了不起,很不幸后来他独自驾船出海失踪了。我对 Jim 非常敬仰,因为我当时正在从研究院去产品部门工作,我就请教他。我说:「Jim,你这一生的职业生涯非常了不起,得了图灵奖,又在研究院工作过,又在产品部门工作过,好像你从来就不介意你到底是在研究院工作,还是在产品部门工作。」他就给了我一个非常好的 lesson,我还记得当时我们是在台北的一个酒店顶楼上交流。他说:「我从来不担心这个问题,到底是在研究院,还是在产品部门。你要选择你哪一个项目,你一生到最后的话,实际上就是你做过哪几个 projects. 」其实我的好朋友高文院士也讲过,他说你的职业生涯到了一定地步以后,大家就看你背上到底写了哪几个字,就是你做过哪几个 projects。特别是我们 50 位得奖的青年才俊,事业起步开始的时候,未来还有很光辉的道路。一定要记住,一个一个项目加起来。
一定要明白自己前进的方向
最后,我想跟大家分享一个,我自己觉得特别是在美国的时候,跟美国的同事讲中庸之道。我有时候讲不过老美的时候,我就跟他们讲「子曾经曰过」,然后我把我想讲的东西跟他们讲一遍。中庸之道。当然以前的路跟现在不一样,现在都分成左右两道。但是中庸之道里面还有一件非常重要的事情,并不是你只是 walk in the middle of the road,这里更重要的一点是 keep the direction,一定要明白自己前进的方向。
重新开始写技术文章
发布时间:2020年11月23日
记得上大学的前两个学期,我很喜欢在第三方平台写技术博客,除了写一些题解外,凡是遇到了什么新奇的好玩的东西都需要记录下来。后来在 IBM Developer 和 MSDN 读了一些技术作家写得比较好的文章,才发现我之前写的哪里是什么“技术博客”啊,分明是些零零散散的“笔记”罢了,再过不久愈发觉得里面的东西实在太没有营养,于是羞愧地把它们给删除了,此后只在静态博客里发布随笔,还写了篇名为《技术博客黑洞》的文章作为吐槽。
随笔里记录思想,其实并不是在务虚,毕竟“正经人谁写日记啊?”只是如果一个人做事情完全只谈方法论而不提具体实践,他不是在扯淡,那就是扯到蛋了。如今要开始写这些过几年后我可能觉得“未必是那么回事儿的东西”,纯属无奈,只希望自己作为老拖延症患者,真的能把这件事坚持下去。
国内的很多程序员写博客,复制粘贴抄袭者居多,有些人把翻译当作原创,慢慢地风气就歪了。甚至如今还有集成多篇文章于一体的究极缝合怪,读起来云里雾里,成功实现了 1 + 1 + 1 = -100 的效果。
命运巧合,当时为了将 Coursera 的 Deep Learning 课程引进国内,中途配合 Andrew 做过一段时间和技术翻译有关的工作,在相关圈子里混迹过一段时间。感慨国内程序员翻译国外文档时的粗制滥造,占着茅坑不拉屎的情况时有出现(比如现在的 Pytorch 授权给 ApacheCN 的中文文档就是一个笑话),部分译者还爱自吹自擂,有甚者更觉得自己是个名家角色了。对于初学者来说,这类翻译读着痛苦,但聊胜于无,英文不好的只能接受,英文要是稍微好些的也不会愿意去看翻译后的文章。毕竟前沿期刊会议论文、官方网站和文档、YouTube 上的视频,难道它们不香吗?因此除了值得被引进,由权威人士翻译校对的经典教材,否则我不推荐任何人找翻译后的中文资源学习。写到这儿,我想起有人曾经在知乎问 Evan You:“为什么 Vue 的更新记录不能有中文,中文文档也一直滞后?” 尤的回答也很干脆:“为啥我要牺牲陪老婆孩子的时间来替你省学英语的时间?”
技术类文章用英文语法来写,有着得天独厚的优势,非常简洁易懂,概念可直接对应源码命名。有的地方硬要译成中文的话,感觉就像“我要用靴子踢你的屁股”一样奇怪。所以即使是国产开源项目,我依旧推荐优先写英文文档。写 API 文档算是比较轻松的,写教程的话难度高了那么一点儿,而要写好技术文章,如果经过翻译这一道工序,总不如原滋原味,所以要么让一个英语和技术功底都好的人来做翻译,要么简直写原创性的有营养的文章。我厌恶现在的 AI 自媒体,也正是因为他们从 Medium 等国外媒体网站或科技公司的官方博客里翻译几篇文章,就可以发在自己的公众号里了,是否经过授权我不清楚,问题在于有时译得不准确;若是看到些中文原创的内容,大部分时候只是在恰饭罢了,这就是流量变现吧!然后你就会看到一堆人前赴后继地模仿,各种抄袭洗稿,去运营粗制滥造的公众号。
没办法,这个时代的人们喜欢以流量为指标,这就导致很多人为了完成相关 OKR 而不择手段,只在乎结果,不在乎过程和意义是否正确,导致表面光鲜的背后隐藏着巨大的风险。今天早上听了汪海林老师在《不要再让选秀艺人影响影视行业了!》视频里说的话,我在想写作平台何尝不是如此,互联网媒体何尝不是如此,主流价值观都要被资本的手段给玩歪了。越傻 X 的东西越有人看,等到有人写干货出来了就一起诋毁它,劣币驱逐良币,大家越来越不知道什么是好作品,反而不断跟风弄出更多 Bull Shit.
如今天元 MegEngine 开源了,版本也到了 1.0 正式版,社区里却几乎看不到什么面向初学者的原创文章。一个如此极客的深度学习框架,缺少 Storyteller,或者说 Developer Advocate,这种情况实在让人着急。毕竟 Tensorflow 和 Pytorch 的生态已经趋于成熟,作为国产框架要进行追赶实在很难办。
然而干着急也不是办法,事情还是得有人干,不论将来是否有成效,总归要下定决心试它一试。目前打算在自己的博客里面瞎写点,等内容和质量上来后再分享出去,虽说客观真理需要我们在不断的认知刷新中求得,但也希望自己不要在当前阶段写出什么误人子弟的东西。
原文出处:https://jmmv.dev/2021/04/always-be-quitting.html
A good philosophy to live by at work is to “always be quitting”. No, don’t be constantly thinking of leaving your job 😱. But act as if you might leave on short notice 😎. Counterintuitively, this will make you a better engineer and open up growth opportunities. A thread 👇. / 在工作中,一个好的生活哲学是“总是准备离职”。不,不是让你一直想着离开你的工作😱。但是要表现得好像你可能会在短时间内离开😎。与直觉相反,这会使你成为一个更好的工程师,并开启成长的机会。以下是具体的做法👇。
So what does it mean to always be quitting? It means “making yourself replaceable”; “deprecating yourself”; “automating yourself out of your job”. You might have heard these more-popular names (which you’ll need to do your own research) and they hint at how to act. / “总是准备离职” 具有什么含义呢?它意味着“让自己可以被替代”;“让自己过时”;“把自己从工作中自动化出去”。你可能听过这些更时髦的说法(你得自己琢磨琢磨),它们暗示了如何行动。
The key lies in NOT being indispensable. If you are, you’ll be stuck at your specific job for as long as that job is relevant with little chance to disconnect (no vacations, no growth). And when (not if) the job becomes unnecessary, so will your position. / 关键在于不要让自己变得不可或缺。否则的话,你会在你的特定工作中停滞不前,只要那个工作还有意义,你就很难脱身(没有假期,没有成长)。而一旦(这不是假设)工作变得不必要时,你的位置也会变得不必要。
Paradoxically, by being disposable, you free yourself. You make it easier for yourself to grow into a higher-level role and you make it easier for yourself to change the projects you work on. Confused still? Here are 10 specific things you can do: / 矛盾的是,通过让自己变得可有可无,你解放了自己。你让自己更容易成长为一个更高级别的角色,你让自己更容易改变你工作的项目。还是感到困惑?以下是你可以做的 10 件具体事情:
📕 Document your knowledge. Every time someone asks you a question, they are highlighting a gap in the documentation. Take the chance to write the answer down (in a document, bug, code comment—wherever) so that the next person doesn’t need YOU. / 📕 记录你的知识。每次有人问你一个问题,他们都在突出文档中的一个缺口。抓住机会把答案写下来(在文档、bug、代码注释——无论哪里),这样下一个人就不需要你了。
🏁 Document your long-term plans. People should know what’s coming up in your projects and/or team by looking at those plans, not by relying on you to tell them “in real time”. Plan a few months ahead so, if you leave, your peers won’t be lost from day one. / 🏁 记录你的长期计划。人们应该通过查看这些计划,而不是依赖你“实时”告诉他们,来了解你的项目和/或团队即将发生什么。提前几个月计划,这样,如果你离开,你的同事们从第一天开始就不会迷失。
🤝 Document your meetings. Keep (public, within the team) notes for all meetings you attend, listing who was there, what was discussed, and any conclusions. Reference those notes from design documents. Your replacement will need these to catch up. / 🤝 记录你的会议。保留你参加的所有会议的(公开的,团队内的)笔记,列出谁在那里,讨论了什么,以及任何结论。从设计文档中引用这些笔记。你的替代者需要这些来赶上。
🚶♂️ Bring others to meetings. If not a 1-on-1 and you are the only person from your team attending a meeting, involve someone else. Different perspectives are useful, but more importantly, you are avoiding becoming the only point of contact. / 🚶♂️ 把其他人带到会议上。如果不是一对一的,而你是你的团队中唯一参加会议的人,那么就让别人参与进来。不同的观点是有用的,但更重要的是,你避免了成为唯一的联系点。
👩🔧 Train people around you. The goal is for them to be independent (what is usually considered “seniority” in a typical engineering ladder). Familiarize them with the plans and technologies and make sure they know how to use the documentation. / 👩🔧 培训你周围的人。目标是让他们独立(这通常被认为是典型的工程阶梯中的“资深”)。让他们熟悉计划和技术,并确保他们知道如何使用文档。
👩🎓 Identify and train your replacement. In the same vein as training others, to switch roles you’ll need to replace yourself. Identify who that replacement might be and actively and continuously coach them. / 👩🎓 识别并培训你的替代者。与培训他人一样,要换角色,你需要替换自己。确定可能的替代者是谁,并积极持续地指导他们。
🔑 Give power to the people. Trust them to do the right thing. If you are in a leadership position, don’t make it so people come to you asking for permission. Let them make their own choices. Guide them so that their choices are based on the right data. / 🔑 给人民权力。相信他们会做正确的事情。如果你处于领导地位,不要让人们来找你寻求许可。让他们做出自己的选择。指导他们,使他们的选择基于正确的数据。
📧 Do not make yourself the point of contact. Establish mailing lists or other forms of communication that can accommodate other people, and then grow those groups. (The exception is when management needs names for accountability.) / 📧 不要让自己成为联系点。建立可以容纳其他人的邮件列表或其他形式的通信,然后发展这些群体。(例外情况是当管理层需要负责人的名字时。)
👨💼 Delegate. Once you have given power to others, included them in groups and meetings, and documented your knowledge, they’ll be ready to take work from you. Delegate work that can make them grow and focus on the things only you can do. / 👨💼 委派。一旦你给了他人权力,让他们参加了小组和会议,并记录了你的知识,他们就准备好从你那里接手工作了。委派可以让他们成长的工作,并专注于只有你能做的事情。
🏫 Always be learning. Take the chance to grow your knowledge in any area you are interested in, and keep it fun. Bonus points if that area aligns with the future path you want to take. / 🏫 始终在学习。抓住机会在你感兴趣的任何领域增长你的知识,并保持乐趣。如果那个领域与你想走的未来道路一致,那就更好了。
Note that nothing here implies abdicating responsibility. You still have to be responsible for all the projects and teams you own, and you have to be for as long as you are in your role. This is important because this responsibility is what will open up new gates. / 注意,这里没有任何东西暗示放弃责任。你仍然必须对你拥有的所有项目和团队负责,只要你还在你的角色中,你就必须负责。这一点很重要,因为这个责任是打开新大门的关键。
Lastly note that, by doing all of the above, you are actively making your whole team better, not just yourself, even if you are an IC. In fact, you are practicing a subset of the skills sometimes associated with staff/principal+ engineers. / 最后要注意的是,通过做以上所有的事情,你正在积极地使你的整个团队变得更好,而不仅仅是你自己,即使你是一个 IC(Individual Contributor,指的是那些主要通过自己的个人工作成果,而不是通过管理他人来贡献的员工)。事实上,你正在实践一些与员工/主管+工程师有关的技能的子集。
文档中就应该提供为零基础用户服务的教程,这是用户基数最大的群体。毕竟高级用户不是凭空产生的,回想当年我们学习某个知识迷迷惑惑的阶段,是不是突然看到一篇好文章,里面的只言片语或许就让自己茅塞顿开了?早期我们寄希望于社区用户,但得到的几篇教程质量都不达标,我甚至一度怀疑是不是因为国内开发者写博客的门槛太低了导致平均质量已经降下来了。后来觉得还是要从自己身上找原因,Pytorch 最初的教程是核心开发者 Soumith Chintala 自己写的,如今由 Stanford 的 Justin Johnson 进行了补充,MegEngine 也应该有一套官方自己的入门教程。半年前 MegEngine 的官网架构中,教程、文档、API 是三个独立开的东西,大家都在摸着石头过河。而现在的文档,是一个特别大的概念(在上面的表格中你应该了解过了),有充分的发挥想象力的空间,处处可改进,最终要实现的目标是:大家香,才是真的香。
回到给农大学生做支持这件事上,我总不能和对方说:“你们先去把 Pytorch 教程看一遍,然后就会用 MegEngine 了。” 然后露出一个尴尬又不失礼貌的微笑吧?只好让她们先去看 Andrew 的 DL 视频,看不懂的地方就答疑,自己琢磨着开始写 MegEngine 版本的教程作为框架侧的练习材料。这时遇到了第二个难点: 人的思维一旦变复杂,很难还原到初学者视角,我们容易把自己的常识误以为是别人也已经掌握的知识,从而在写教程的时候写出很抽象的内容。 纯粹以个人理解视角写出来的东西也不能算得上是面向大众的教程,很多时候只能说是个人笔记。如果读过糟糕代码风格 + 只言片语的算法题解博客,又接触过良好代码风格 + 切中要点解释的算法题解博客,感触应该会比较深。好在我个人比较幸运,之前曾有过课程助教的经验,平日里也习惯了多角度观察,所以切换视角这个事情对我来说已经不是那么困难。基本上白天写完一篇教程,晚上就会找到些写得不友好的地方。
另外要注意的是,获取教程受众的反馈很重要。 国人比较谦虚,能力越强的人越是喜欢自己把问题研究清楚,等到视野开阔理解能力变强后,就算教程存在着瑕疵,也被过往经验自动完善掉,很难意识到是教程本身存在着不合理的地方。我一直和几个研究生说 “如果觉得没写清楚的地方就多反馈,不要怕问蠢问题。这不会麻烦到我,而是在互帮互助。” 实际上大家的理解能力都在线,真正焦虑的反而是我,怕没讲明白学生却不好意思说,宁可自己花额外的时间去消化,这种情况应该被视作潜在的教学事故。
不是所有人都是天生的教育工作者,Andrew 在 Linkedin 上有一篇很热门的文章,讲的是如何通过刻意练习,让自己成为更受学生欢迎的老师。即使是同样的材料,给不同的讲师去教授,起到的效果也可能截然不同。比如我见到过一些老师用 CS231N 的课件去给学生讲课,但完全体会不到 Andrej Karpathy 讲课时的感觉。再举个例子,直到现在,我还是会偶尔想起当年上算法分析课时的那名教授,他主要研究计算机网络方向,和我们说因为写文章要用到算法,最近开始研究算法导论了,就顺带申请教授这门课,希望和大家一起进步… Ohh 我亲爱的老师,时间就是性命。无端的空耗别人的时间,无异于谋财害命。
一定要使用自己熟悉的教学材料,而不是用大家都觉得好的材料去给学生做临时翻译,一是没有原汁原味的体验,二是容易让其他人对教学材料和原始作者形成抵触和误解。 在写零基础教程系列的时候,我一直在研究如何能够让它变得更加 Beginner friendly, 一个用户说要不要直接用李沐的《动手学深度学习》或者是 Coursera 上面 Deep Learning Specialization 的编程作业,我认为这些材料都很好,但不适合放在 MegEngine 文档里,决定还是自己搞。最初的几版往往都在加内容,担心举例不够;而最后往往都在做减法,去掉一切当前不用过度关注的数学细节。现在看似是完成大体内容了,而我却只能说差强人意,还需要经过长期的检验和更新,自己也不是什么领域大牛,特别怕写出来的东西误人子弟,早就做好了挨骂的准备。教程最后的问题都倾向于让人去主动思考而不是留客观题,因为我是反对培养做题家的, 在学习新知识的初期,建立基本的直觉和浓厚的兴趣是重中之重,至于如何启发思考,把握内容的节奏感和分寸感(甚至像艺术品一样去打磨它们),则是更进阶的话题。好消息是,我们如今能够从研究实习生那儿获得教程的支持,其实任何人都可以尝试提供写教程的思路。
从学习阶段进入使用阶段后后,教程的使命就结束了,用户指南和 API 参考成为了文档中经常被光顾的地方。这就好像一个神经网络模型已经得到了充分的训练,接着我们只需要用训练好的模型去执行推理任务即可,那些费时良久的反向传播和参数优化时光都变成了过去式。除了用户指南,MegEngine 文档中还提供了开发者指南,因为 MegEngine 是一个开源项目,我们希望社区中的开发者能够借助文档中的信息更有效地参与进来,为 MegEngine 的发展作出贡献。指南的写法,和教程还真有挺大的区别。可惜的是由于各种原因,到现在为止,MegEngine 的文档也只是完成了整体拼图中的极小的一块,依旧是“路漫漫其修远兮“,需要“上下求索”。但如今我们有思路啦,找到方向啦,不再是当初摸着石头过河的状态了,并且我坚信它能按照预期设想伴随着 MegEngine 持续发展下去。
前面用了很多篇幅来讲教程,是因为这是前段时间的工作重心,亦对后续的工作有很大启发。然而我们也说过了,教程只是文档世界的一小块拼图,想要更好地理解文档建设的意义和目的,可以有很多理解的视角,这次我选择引用前不久看到的一句话作为解释:
用户通常只会用到软件中 15 % 的功能,而不同类型的用户使用的往往是同一个软件中那不同的 15% 部分。
道理挺容易懂的,以 Microsoft PowerPoint 为例,初级用户往往只需要用到基本的功能,比如图片、文字和页面的编辑;随着需求的改变,有些人研究大纲视图、讲义备注和演讲者模式,有些人研究动画效果,有些人研究如何优雅地插入
公式(关于这个问题,Windows 用户可以试一试 IguanaTex;MacOS 用户可以试试 IguanaTexMac;另外可以使用 Mathpix Snip 进行 OCR 识别),这已经开始进入插件范畴了。如果用户需要的那 15% 无法在某一个软件中(或者以插件的形式)提供,自然地就会去寻找满足需求的同类替代品——比如在 3D 建模领域,建筑、机械类 CAD 平面设计经常用 3DMax,影视动画设计经常用 Maya,游戏建模经常用 Unity/Unreal,包装设计经常用 C4D,雕刻用 ZBrush,还有一个开源的 Blender,目前各项功能都是 60分… 生活中有非常多类似的例子。
从 15% (不用在意这个值是否客观)这个点出发,可以引申出非常多的思考:
一个用户在刚接触到新产品时,学习的是最简单的功能,界面的友好程度、功能上手的难易程度等会直接决定用户的留存。但是由于对不同的用户这 15% 是不同的部分,因此需要分别提供对应的入口。而且用户的属性不是一成不变的,作为软件供应商,还要思考如何帮助用户发展自我,从初级萌新转变为高级资深玩家。这个时候,提供不同入口的教程尤其重要。
从软件研发的角度来看,知道自己在干什么很重要。或许我们正专注于实现这 15% 的关键特性,脑海中知道其中所有的技术细节。但我们有义务形成这样一种大局观,即我的设计在整个系统中扮演着什么样的角色,它又是怎样深刻地影响到上下游各个环节的,可以理解成系统架构的意识。这个时候,信息流程类文档尤其重要,我开始尝试推广切实可行的 MEP 方案。
虽然整个软件系统由很多个所谓的 15% 模块所组成,一些基础设施(底层设施)的 15% 模块是不直接面向用户的,研发过程中不可避免地会面临人员流动的情况。我们如何保障在不破坏原有大体设计思路的情况下进行持续稳定的开发和迭代?如何保障公开接口稳定性?这个时候,设计文档尤其重要,理应存在于开发者指南中。
MegEngine 文档或许已经能够满足某类 15 % 用户的需求了,但这还远远不足够。
我们需要建立这样一个认知, 写文档本质上依旧是在为解放自己的生产力进行铺垫 —— 聪明的程序员都懂得如何偷懒,巴不得能用脚本尽可能地去自动化一切工作。我们可以研发出深度学习框架来简化训推流程,用户也可以基于此提供更加高级的接口或库。但我们往往会忽略这样一个事实:在一个团队中,研发人员的日常绝不仅限于纯粹地去实现代码逻辑功能,很多时候都需要和人交流,大致可以分为以下几种情景:
我们要教会使用者如何去用我们开发出来的东西,因此教程必不可少。
我们要实现一个东西,因此需要找人讨论实现方案,提供 Proposal / Prototype 实现。其中存在着信息的收集、同步、讨论、达成一致决策的过程,如果不采取任何形式的总结,将来很有可能需要向其他的想要了解技术细节的人解释 “当初为什么要这样做(不那样做)” ,更糟糕的情况是,你可能需要将同样的事情向不同的人解释了一遍又一遍;而人的记忆不是永远可靠的(人脑也不是用来机械记忆的工具),我们无法保证将来总是能回忆起曾经的各种决定。一旦产生元老级别的人员流动,很多未来得及记录的信息就像青春岁月一样飘散在风里了。
我们要读懂别人的实现,碰到了困难,然后跑过去问实现者。对方可能不一定有空,但强者往往又表现得十分友善,基本上有问必答,最终会占据掉原本用来安排进行其它工作的时间;当然你也可以询问其他了解这块的人,但口口相传的方式更加无法保障信息的准确性, 我们需要制定流程规范,目的是为了提升整体的工作效率,口头约定并不具有约束效果;
随着工作经验的丰富,我们开始指导实习生的日常工作。一些我们觉得属于常识的东西,可能对于新人来说就是全新的事物,尤其是限定于项目中的一些细节,毕竟一些坑可能永远不会在其它项目中遇见。虽然我认同 “该踩的坑终究还是要自己踩一遍”的说法,这样印象会比较深刻。但作为过来人,提供辅助视角作为参考,会更有帮助;甚至我们可以刻意地去设计一些优化过的坑来帮助实习生更好地成长,减少其它的干扰项。
即使是写教程,也会因为我们之前提到的 15% 理论而需要达到 “因材施教“ 的效果。例如有的用户当前只想学习如何训练模型、有的用户只想学习如何推理部署;再细化一些,有的用户只想知道多机怎么用,而有的用户只想搞明白量化的原理。这个时候,文档需要针对这不同的 15% 用户,分别提供最合适的入门教程。比较常见的情况是,用户在弄明白如何去训练一个模型后,有可能开始对模型部署感兴趣了;而对另外一部分的用户来说,可能完全没有阅读过训练教程,只希望能够用现成的模型进行部署。同样地,从学习阶段过渡到使用阶段后,用户更需要的是简明的指南。
文档的另外一个作用是服务于项目本身的开发者,可我们是那么地讨厌写文档,又那么地讨厌别人的项目没好好写文档。这时候不得不对比一下“直接交流”这种形式,很多人都会以口头交流更高效为由拒绝写文档。诚然,相较于打字聊天这种形式,面对面的交流可以和帮助我们保持专注和高效。但如果是频繁地解释已经实现的东西,其长期效益未必高过写文档。毕竟不是所有人都是语言大师,交谈过程中废话的比例有时候会比预想的多得多,想要强行地让别人兼容自己的思维模式,亦或者是做到对他人的兼容,都是极端困难的,需要大量的练习。如果不在正式的交流开始之前拉齐双方的认知,可能聊着聊着发现原来不在同一个频道。
很多时候在一场对话中,我们可能会觉得 “怎么讲了这么久,对方还是不明白?是不是理解能力有问题啊?” ,而对方可能早就在内心吐槽 “这个人讲的是什么东西啊?根本没把东西讲清楚,是不是表达能力有问题啊?他是不是根本不知道我问的是什么?”,总之锅肯定是对方的就没问题啦!与人相处是值得一生去探究的话题,而对话时进行的一些假设其实是我们与生俱来的弱点,对他人抱有严格的期待,而容易忽视掉自己身上不足的部分。不欢而散的情况也是有可能产生的,这其实还算好,更糟糕的情况是为了避免尴尬而不懂装懂,误认为这次交流是有效的,结果又要用更多的交流来填坑,我们真的做到科学高效了吗?
我在与人相处的时候有一个比较常用的小技巧,即借助于对方已经输出的事物来快速寻找共识。怎么理解这句话呢?要想了解一个人,直接问另外一个人 “你觉得这个人怎么样?” 得到的答案难免带点主观,要么被打哈哈糊弄过去;直接去和对方交流呢又会比较花时间,因此我倾向于去寻找并阅读他/她对外输出的内容,比如邮件、演讲、讲座、论文、博客、歌单、视频等等,尝试去构筑对方的认知体系,习惯他/她的表达方式,看能否找到融合点,这个探索过程通常是让人欣喜的,我们也在拓宽自己的可接受范围。应用于文档上面,则演变成我们在讨论技术问题的时候可以交流双方对同一篇文档的理解是否一致,不一致的话则求同存异,最好的情况是大家都得到了新的思路或者是观点,同时文档的内容也能够更新。如果觉得文档写得不好,吐槽文档的内容就是了(即对事不对人),然后一起分析怎样去改进它——只是交流而不做记录的话,很容易遗忘甚至是产生错误的记忆。通过总结文档的形式,让每一次的交流都能以某种形式沉淀下来,长期来看会节省非常多的时间成本,如若帮助到了别人,无形之中可能就放大了之前讨论的价值,推荐大家都试一试这种无情绪压力负担的输出方式。
我还想稍微提一下版式设计方面的细节,这是一种无意识地改善用户体验的途径。在这个创意无处不在的时代,越来越多的人成为设计师。简历、论文、PPT、个人主页、博客、活动海报、给客人的邮件、名片……,处处都在考验你的设计能力。几乎每次提到产品设计时,我都会和人安利 Robin Williams 的 《The Non-Designer’s Design Book》,时刻注意并尝试练习满足 “亲密性”、“对齐“、“重复“和“对比” 四大原则。在文档中体现的比较明显的地方是:我们会经常用到一些视觉样式元素,比如 note/warning 来避免过多的重复,形成局部的对比,否则用户读起来会很累。整个文档的配色风格也需要一致,字体图片的样式、段落的间距,处处都是可供改善的细节。
我还想要强调一下已知常见文档形式的局限性,以及现代化文档的发展趋势。由于历史原因,很多文档要求以纯文本的形式进行提供,这样对文本编辑器友好。但在一些概念解释的情景下,往往是一图胜千言的,因此我们需要主动地拥抱表达形式的变化,尝试用更加新颖的媒体手段进行创作和表达。一种比较好的实践模式是,首先想清楚你想要输出内容的范围,对应的受众有哪些。这个时候可以列举出一个模糊的大纲(可以参考一些写作范式),但不用要求它一开始就在内容上达到很完美的程度。经过一段时间的折腾后,我们应该能够得到最原始的文本,往往只能用于审视内容结构,无法达到直接发布的程度,我们可以根据版式设计的原则去添加合适的图片,正确的配图能帮助读者更好地理解内容。
如今不可忽视的一种内容表达载体的形式是视频,比较常见的做法是在一篇教程中,标题下方是一个嵌入的视频,紧接着是对应的文字脚本或者内容总结,将来的 MegEngine 文档应该也会进行这方面的尝试。在线教育是大势所趋,但可惜的是很多人入场探索,却只学习到了视频的术,认知停留在“我们需要录个像,把人脸放上去让用户感到被重视”的程度。一些人的目的本就是趁着这波红利卷了钱就跑,并不试图进一步明白怎样通过视频更加有效地传递信息,哪里有什么教育的初心?一些不专业的细节处理往往还会往视频中引入过多的噪声,降低信噪比,加大用户的认知成本。比如 PPT 风格的讲解视频内容中最容易被忽视的一个因素是音频的质量,试想一下我们在进行会议的时候,经常会出现一些人都发言不清晰的情况,这个时候恐怕就没有精力去捕捉里面的细节信息了,成本太高;所以糟糕的音质比糟糕的画面更容易让人失去耐心。音频后期技巧还包括去除口水音、去除齿音等技巧,其实如果一个人的普通话不标准,也有可能劝退一些听众。BGM 也很重要,用得好是加分项,用得敷衍就容易折磨观众的耳朵。在细节处不求做的完美,但不能差到拉低了整体的下限。
如果是实拍视频,还需要考虑到摄影(画面)方面的一些细节。比如一些代码演示的视频中,如果出现了敲错代码,或者是讲着讲着发现思路不清晰甚至讲错的情况,请务必重新录制,严谨一些!不要让观众因为你的失误而付出代价。至于视频画面的内容, 3Blue1Brown 的风格是我目前最推荐的用来解释抽象概念的途径,在 MegEngine 的入门教程中也有推荐。我们应该以工业级的标准去打磨我们的视频质量,想想这个镜头想要表达什么,需要用到什么样的视觉效果才是最自然的,节奏感对不对,是不是这个情节可以放在后面作为压轴?画面中的留白、实际内容比例也会直接影响到观众的体验… 只有我们将可能存在的干扰项优化到极致了,用户才能专注于感知核心内容的表达。直接鼓捣出一个视频是很困难的,得一步一个脚印慢慢走。而我们的写作,其实就是积累原始表达内容的方式,写作的过程中我们的头脑会一直在思考,刚开始可能会觉得这让人痛苦,但久而久之,你会发现找个地方安静地写作,是浮躁世界静下心来与自己对话的一种方式。
另外,将来的人们在发布内容时,一定会做更多在交互性上面的尝试,正如 https://distill.pub/ 现在所做的(编辑最近写了一篇《Distill Hiatus》,认为大多数类型的文章的未来可能是自我出版,值得一读)。从把一件事情做完,到把一件事情做好,基本上会慢慢地走上一条未曾设想的道路,我们在不断地拓展自己的认知边界,最终完成意识形态上的升级,将来在做其它事情的时候,或许能得到一定的启发。
我们可以用一张表来表示体系完整的文档应该覆盖到哪些情景:
学习阶段 使用阶段
实践步骤 教程 🤔 指南 📖
理论知识 解释 📝 参考 📚
教程(Tutorial):指导读者通过一系列步骤完成项目(或有意义的练习)的课程;
指南(Guide):引导读者完成解决常见问题所需步骤的指南(How-to 系列);
参考(Reference):用于 API 查询和浏览,方便网络引擎检索的百科全书;
解释(Explaination):对于特定主题、特性的概念性说明。
收集一些临时文字。
pen 钢笔
pencil 铅笔
ballpen 圆珠笔
eraser 橡皮,黑板擦
ruler 格尺
penci-box 铅笔盒
sharpener 卷笔刀
draft “草稿”,指方案、计划、报告、合同等文字草稿。
例句:
My secretary has typed out the first draft of the report, but I must revise it before I submit it to the conference.
我的秘书已经打印好报告的第一稿,但我还必须在提交给会议之前把它修改一下。
The chairman went over the draft of the report and make some corrections on it.
主席审阅了报告草案并作了几次修改。
A draft of the new rule is set to be approved by the Federal Reserve Board and put out for public comment within weeks.
一份新规草案将在几周内获得美联储董事会通过,并公开征求意见。
We will give you a definite reply after we have studied the details of the draft contract.
我们研究合同草本的细节后会给你们一个肯定答复。
script 指剧本、电影脚本、广播稿,讲话稿。
例句:
Two writers collaborated on the script for the movie.
两位作家合作编写了这部电影的剧本。
During certain scenes of the play there isn’t any script and the actors just improvise.
在戏剧的某些场景中,没有剧本,演员们只是即兴发挥。
It’s a great idea for a show but the script lacks a little sparkle.
这是一个很棒的想法,但讲稿缺乏一点亮点。
The producer disliked the script and demanded a rewrite.
制片人不喜欢这个剧本,要求重写。
Have you got the script for the listening in class?
你拿到课堂听力的原稿了吗?
manuscript 手稿、原稿;手写本,手抄本。
例句:
He sent the 400-page manuscript to his publisher.
他将400页的手稿寄给了他的出版商。
I hunted down the manuscript in the British Museum.
我在大英博物馆找到了这份手稿。
It is thought that the maunscript is the work of a monk and dates from the twelfth century.
那部手抄本被认为出自12世纪一位僧侣之手。
sketch “草图”、指绘略图、素描或写生。
例句:
The report contains several sketches of the new device and a verbal description.
这份报告包括几份新装置草图和一段文字描述。
The artists usually make several sketches before the final painting.
艺术家在最终作画前经常会先画几张草图。
He drew a rough sketch for us to show how we can get the destination.
他为我们画了一幅草图,标明我们如何才能到达目的地。
He did a sketch of the ballet dancers.
他为这些芭蕾舞者画了一张素描。
She drew a sketch map of the area to show us the way.
她画了一幅该地区的草图给我们指路。
人工智能生成时代,已经不知道写什么了
随着人工智能的发展,大多数的基础内容产出已经变得毫无意义了,首当其冲的是技术社区。 放在几年前,简中技术社区的绝大部分文章基本上都是对官方英文文档、教程等的翻译。 处于新手期的人们把它记录下来,以备自己查阅,也方便他人学习, 但现在你只需要把文档和需求、或者是报错信息丢给大语言模型,问题基本就能解决, 甚至不用你写任何一行代码(没准将来的文档也全是人工智能写的)。 而且随着人工智能模型的进一步训练,这种能力会越来越强。 此类技术文章只有在深度和广度上有所突破,才能有更多的价值。 [2]
比如我的上一篇关于 AMD 内存泄漏 的文章, 这种问题无法通过人工智能得到解决方案,因此就有了被单独记录下来的价值。 但将来它一定会成为人工智能的训练数据,以便更好地帮助人类解决类似问题。 ↩︎
除了技术文章和代码生成,更加值得警惕的是,我们需要经过一个各种形式的内容产出良莠不齐的时期, 因为人工智能极大地减低了内容的门槛,导致整个创作生态变得混乱。 这种混乱不仅体现在写作领域,还有听书等类型的应用,绝大部分有声书主播(声优)预计三年内就会被人工智能取代掉。 我的父亲很喜欢听单田芳的评书,可惜大师已经去世了,而 AI 单田芳已经初见雏形。 至于短视频领域,似乎遭受了更大的低成本劣质创作的 “迫害”… 这种大趋势固然是好的,只是在发展初期,内容和版权均缺少有效的监管和规范。 我想,人工智能是为了帮我们解放生产力,挑战更加困难的领域,而不是用有限的生产力去创造尽可能多的垃圾。
至少在 AIGC 领域,我坚信将来会出现先驱者,告诉我们 AIGC 的最佳实践方向。
如此看来,关乎个人的非技术创作似乎成为了不被侵扰的净土,可于我而言这也是矛盾的。 如果只是记录自己的生活,我可能不会选择公开自己的动态在个人博客,因为有私有化部署的 NAS 应用。 分享自己的想法吧,太保守显得平庸,太激进显得危险,也有可能成为日后被党同伐异、口诛笔伐的材料,实在是如履薄冰。 [3] 成年人的社会除了利益交换,剩下的表达好像只剩下 “关我屁事” 和 “关你屁事” 了。 慢慢地,一切的行为都要为了某种利益,而不是为了自己的兴趣和爱好。 如果一个人的观点受到了广泛的关注,可能并不一定是因为其中具备多少客观的真理, 而是因为它的内容能够引起人们的共鸣,缓解了一些焦虑,解决了部分实际的需求。 也有可能是因为这个人已经取得了世俗的成功,人们更倾向于相信他的观点。 另一方面,很多人在网络上浏览内容并不是为了解决核心问题,而是为了消磨时间,找到一些可以让自己感到愉悦的东西。 这也是为什么成功学、觉醒学、心理学、情感学大师总能够在网络上找到一席之地。 要知道中国人都是信奉闷声发大财(和窝里斗)的,永远不要相信在网上花钱就一定能学到真东西, 除非培训你会给对方带来更大的价值。
如果沉默的成为大多数,那么少数的发声只因有利可图。 ↩︎
回想起自己曾经在 技术博客黑洞 对创作提出的要求—— 写出 50 年后,甚至 100 年后的人们读起来依旧能有启发的作品。 内心不禁在询问自己,我真的做到了吗? 所以还是得慢慢沉淀,就像部分潜心搞科研的人一样,不断地积累,不断地思考,不断地总结,最后搞点新花样 [4] ,没准无心插柳妙手偶得,忽地发现就造福人类了。
前提是你的论文不能是 AI 帮你写的… ↩︎
如果你的创作内容十有八九是可以完全由人工智能生成的,那就值得考量考量该不该在这部分内容上投入时间和精力了。 在不遥远的将来,基于人工智能的问答将成为我们日常生活的一部分,这是不可逆的趋势 —— 人人都能成为拥有贾维斯的托尼·斯塔克(钢铁侠)!
综上所述,除却对时间的功利安排外, 我的创作欲望已经低无可低,加上对自己的要求也越来越高,也不自信短期内能产出有价值的内容。 所以 xLog 这个平台对我来说,可能只是一个注册了账号的空壳。 期待 xLog 平台能搞出更多有吸引力的功能,让我有动力去尝试一下。
骆驼玩法
什么是骆驼玩法?
使用 Git LFS(Large File Storage)来管理这些二进制文件, LFS 的原理是:在 Git 仓库中只存储指针文件,而真正的文件存储在 LFS 服务器上,按需获取。
version https://git-lfs.github.com/spec/v1
oid sha256:4cac19622fc3ada9c0fdeadb33f88f367b541f38b89102a3f1261ac81fd5bcb5
size 84977953
讲到这里,不妨提一下“博客”的由来。“博客”(又称“部落格”) 一词来自于英文“blog” , 最初 Jorn Barger 在1997年12月17日提出了 “weblog” 的概念,即“网页”(web) 与 “记录”(log) 的组合; 而后由 Peter Merholz 开玩笑地拆分为“我们”(we) 与 blog 的组合, 后续术语 blogger 的出现,使这一概念慢慢被普及和接受。 不难看出在那时的背景下,大众需要的是能够在互联网上编辑和发布内容的产品,最好能和其他人产生社交互动。 如今微博和微信朋友圈的出现,让网络社交变得更便捷,自媒体公众号成为了博客的替代品,这是技术的进步。
缘起
一直想拥有一个属于自己的独立博客,这或许算是我作为程序员的执念之一吧。
为什么
因为我从别人的博客中收益良多。像学习 Vim 的时候拜读过酷壳的 简明 Vim 练级攻略,还包括谷歌搜索时经常接触的其他优秀文章,期待有一天我也可以通过文章来帮助别人。
希望认识更多志同道合的朋友,人生太短,世界太大,如果有一天因为博客而结缘,进而成为朋友,不是很美妙的事情吗。这几年我通过博客认识了好几位朋友,虽然不多,但是都保持着不错的关系,我也期待着别人通过我的博客认识我的那一天。
希望能有一个自留地,有些话想说,有些事想做,有些回忆想留下。而假手他人,总不如自己可靠。
为什么一定要独立博客?
不可以依赖公众号、CSDN 等博客系统吗?曾经的我也是这样想的,为什么一定要这么麻烦,重要的不是写文章这件事吗?是的,最重要的是开始写。我也曾经使用静态博客、Github Isuues,以及公众号的方式来托管文章,但是随着长期使用它们总有不尽如人意的地方,后面会在我的独立博客史中谈到。核心的问题在于:我们并不能真正享有我们文章的所有权。
我的独立博客史
第一个博客,大学时代的初次尝试:WordPress
我在学生时代早期一直有记日记的习惯,然而到了高中后期因为学业的原因放弃了,除了不久前又尝试记录过一段时间,从此再也没有捡起来过。
到了七八年前的大学时代,希望能有一个地方记录我生活的念头再次燃起,这时候瞄准了功能强大的 WordPress,使用了当时新潮的 .me 域名,尤记得我的服务器是申请了 Github 的教育优惠,用领的 100 美金代金卷购买的 DigitalOcean 最便宜的 5 美刀一月主机,好不容易搭建上线之后发现因为外网的缘故,国内访问非常卡,而且 WordPress 本身非常臃肿,体验并不好,在断断续续写了几篇文章后就废弃了。
第二个博客,毕业后再起航:Jekyll 静态博客
时间荏苒,大学很快结束了,我于 2015 年 6 月底毕业,工作之后接触的多了,再次希望能有一个地方呼吸除了工作之外的清新空气。
第二个博客吸取了教训,为了专注写文章这件事情,选择了 Jekyll 这种静态博客,托管于 Github,于 2015 年 8 月 17 日上线。在这里第一次认识到 Markdown 文件格式,后面一直基于此编写文章。博客上线初期写了几篇文章,后面因为系统故障,本地原始文件丢失,然后才发现 Jeklly 竟然只保存了渲染后的静态 html。
原始文件的丢失加换工作的缘故,进入了 9106 的高强度模式,情绪低落就没有继续写了。而博客也随着 .me 域名的到期而关闭,只留下 Github 的仓库遗迹。
jFDUxi
第三个博客,不死心之再出击: Github Issues
正所谓念念不忘,必有回响。在第二个博客关闭的两年半后,2018年 3 月5 日,我选择了 Github 的 Issues 继续我的博客之旅。
经过两次失败的沉痛反思和近五年的时光,我认识到不管使用如何花哨的技术,如果我不能保证持续的产出文章的话,我的独立博客之旅就要在此终结。我不甘心啊!
我告诉自己,这次我一定可以成功,因为我的独立博客之魂还在燃烧。而最重要的就是专注写文章,专注写文章,专注写文章。
这也是我选择 Github Issues 的方式,简洁、无广告,同时把 Blog 仓库置顶到首页,作为一个每天都上 Github 的来说,也是对自己的一个提醒和激励。
S6MX9w
这次我成功了,在花了一年多写了十来篇博客后,虽然产量不高,但是我发现坚持写文章对我来说已经不是问题了(如何坚持有机会额外分享)。而这时 Github Issues 的缺点也渐渐暴露了出来。
贴图困难。我之前都是手动截图,传到 Github 仓库,然后链接原始文件地址,非常繁琐。后面贴图的问题通过 upic 这个小工具有所缓解
分享困难。我有些读书笔记想和我的朋友们分享,但不是所有人都是程序员,访问 Github 对某些人来说太不方便了。
交流困难。因为分享困难,导致我的文章基本没有互动,除了第一篇文章有 10 来条捧场评论,后面在两年间通常只有一到两条评论。
没有统计。对于文章的相关数据基本一无所知,再加上评论的稀少,给自己一种玩单机游戏的感觉。
没有订阅。虽然有 Watch,但是功能还是太弱了,基本上可以说是可有可无。
第四个博客,有苦有乐:公众号
这时候我了解到了公众号这个平台,觉得对以上几个问题是比较好的解决方式。一开始是在 Github 发布文章之后再手动同步到公众号,但是很快我就喜欢上了公众号的体验。
zSEWiR
文章在公众号的阅读量和活跃度比在 Github Issues 高的多,而且跟朋友的交流也很方便,附带的数据统计也够用。这时因为贴图过于繁琐的问题,我渐渐不再同步内容到 Github Issues 上了。
在公众号写作了快一年后,公众号的缺陷也慢慢暴露出来了。
更新文章受限。文章发出后,只能有一次机会修改二十五个字。有时候只能通过留言来纠正,体验并不好。
广告嫌疑太浓。公众号目前的营销和商业氛围太浓。有时候跟朋友或者在群里聊天,都不敢发自己的公众号文章链接,总感觉自己是在打广告,特别羞耻。
无法被搜索引擎收录。文章的阅读数集中在发表后的一两天内,之后基本不再增加,除了特意翻看历史文章的人,基本不再有机会被曝光。
无法外链。之前我经常外链 Wiki、Github 等丰富文章内容,而公众号只提供一个阅读原文,有时候真的让人感觉非常不便。
自定义功能太弱。比如我想跟朋友交换友链、加上作品列表、支持 RSS 订阅、放上 Github 链接、这在公众号的系统中是无法满足的。
第五个博客,浴火重生:自己开发 shidenggui.com
其实很早前就动过开发博客系统的念头,本身作为一个后端工程师,博客系统可说是非常简单的应用。但是真正的问题在于前端,之前也曾自学过 CSS 和 Javascript,但 CSS 给我留下了难以掌握的心理阴影,尤其是其中的 Float 布局。
不过最近一年事情出现了转机,我花了不少时间打磨我的小说推荐网站 推书君(www.tuishujun.com) 以及对应的小程序版本,在这个过程中我渐渐熟悉了前端,虽然还称不上专业,但是我想开发一个简单的博客应该是够了。
时机成熟了,我告诉自己,而且正逢春节在家隔离,时间充裕。说动就动,在吸取了前几个博客的教训之后,做了以下几点考虑:
文章依旧使用 Markdown 编写,显示的时候渲染为 HTML。现在有很多优秀的 Markdown 编辑器,比如 typora 等,没有必要再实现一套复杂的编辑系统。而文章的元信息比如创建时间等参考了 hexo ,使用 YAML 附加于文章头部。
支持搜索引擎收录。使用支持服务端渲染的 SSR 框架 Nextjs。
保证加载速度。CSS 使用 Tailwincss,基本没有单独写过样式类,CSS 大小控制在 5K 以下。尽量减少 Javascript 类库的引入。使用 CDN 进行加速。
提供 RSS 和公众号两种订阅方式。
尽量小的内存占用,降低后续维护成本。使用 Docker 打包,整个博客包括使用 Nestjs 的后端,内存占用 170 MB。
总体开发花了三天时间,其中一天用于将公众号和 Github 的文章改为当前的 Markdown 格式,于 2020 年 2 月 14 日正式上线。
独立博客上线一周了,带来了什么变化?
说实话,效果有点超出我的预料,容我细细道来。
友链
博客上线之后,第一步自然就是跟朋友交换友链了,终于给朋友们的博客地址找了一个好家。
shidenggui.com_links(iPhoneX)
更好的文章分享支持
在发推或者 tg 群里面聊天的时候,常常会看到别人贴的链接有特效,而自己贴的文章却不行,研究后发现是需要在网页中添加关于社会化分享的配置,这次也给自己的博客加上了。
f0gjam
更好的传播
程序员的交流还有很多一部分不处于微信生态圈,而大家也大多不愿意分享微信文章链接。
博客上线一周之后,我遇到了一个针对开源开发者的福利活动,整体流程比较复杂,有不少朋友遇到问题,我就写了一篇自己的操作流程分享到博客上,看能不能对大家提供帮助。没想到大家非常热情,也有不少朋友写文章的时候顺带提了一句,结果这篇文章在三天不到的时间收获了两万多的阅读。
fuwEnm
虽然是暂时的,不过还是很高兴能给那么多人带来帮助,也算是给自己的博客开了一个好头。
一些小问题
图床:图片的存储一直是独立博客比较烦恼的问题,我这次使用的是 gitee 的图床,当后来发现图片无法在微信浏览器中正常加载,比较恼人,后续准备换用自己的存储系统,具体还在调研中。
原创保护:之前我在公众号翻译过一篇谈产品的文章,反响比较热烈,好几个人找我开转载白名单。而这次发布的教程文章同样受到好评,但是我在一天后发现 segmentfault 和知乎已经有了我的盗文,而我却没什么太好的办法。
尾声
博客刚刚上线一周,有很多小细节还在优化中,有些功能尚待开发,不过也收获了很多惊喜,后面我也会持续努力。
以一段话纪念我这曲折的独立博客史:
天方国古有神鸟,名「菲尼克斯」(Phoenix),满五百岁后,集香木自焚,复从死灰中更生,鲜美异常,不再死。 ————《凤凰涅槃》
风格就是作者看待世界的方式。
古典风格
古典风格崇尚简洁和明晰。它的隐喻是观看世界,作者不过是引导读者去看他所看到的。它相信读者有能力辨别事实,避免琐碎的解释和冗长的论证。
基本立场
Truth:存在真理,而非怀疑和相对主义
Truth can be known:真理是可知的,只要运用理性
Truth is not contingent:真理是重要的,绝非琐碎
Truth is pure:真理是纯粹的,而不是煽情或恳求
The motive is truth:为了真理而写,其他不过是附带的
Presentation
Prose is a window:重要的是世界而非窗户
Prose is a perfect performance:果断、简练而非婆婆妈妈
Every word counts:不推荐略读,没有废话
Clarity everywhere is not accuracy everywhere:简化无关主旨的论证
Sense
Informal talking with friends authentically:跟朋友之间非正式的真诚交流
Reader is competent:作者比读者多的只不过是信息,只要读者愿意,他永远有能力获取这些信息来验证作者的论述
古典风格让读者觉得自己就像天才,糟糕的文章让读者觉得自己就是蠢材
对于古典风格的文章,读者“可能会得出结论,它精湛、古典,但完全错了”
古典风格的局限性
有些真理无法言说,这就限制了适用范围
所言说的是否客观无从验证,只能作为先验的假设
对比
朴素风格
“早起的鸟儿有虫吃”是朴素风格,“早起的鸟儿有虫吃,但第二只老鼠有奶酪吃”(第一只老鼠被捕兽夹抓住了)才是古典风格。
朴素风格认为真理简单而直接,不需要深入的思考,也不需要论证,所见即所得,却忘记了人可能会被情绪、利益、文化所遮蔽。这正是古典风格所强调的:真理简单却绝不直接。
真相属于所有致力于获得它的人,但肯定不是人人都有,也不是任何人的天生权利
实用风格
同样注重简洁,但实用风格是因为明确的目的性和时间限制,常见于商业计划书等,作者是为读者服务的。古典风格的的简洁却是出自“思想的优雅”。
反思风格
若说古典风格的隐喻是观看世界的话,那么反思风格就是灵魂发问:观看是可能的吗?世界真的存在吗?观看是有意义的吗?
沉思风格
沉思风格相比呈现更注重附加的诠释,它不相信单凭观看读者便能看见作者所见的。
浪漫风格
如果说古典风格是一扇窗户,那么浪漫风格就像是一面镜子。浪漫风格是作者经验的流泻,无关世界。
人生中所有最严重而且最重要的问题,基本上都是无解的 — 《最小阻力之路》
无解的问题
我们的选择常常受限于环境。如果将环境视为一种建筑结构的话,潜藏的结构固定后,也就决定了我们将走这道门而不是那扇窗。虽然并没有人规定我们不能这样做,但违反结构的行为往往吃力不讨好。
由此可知人类的行为与其生活的潜藏结构是相符的。人生犹如河流,沿着最小阻力之路流动,生活中的潜在结构塑造了一路径。当我们尝试改变行为但没有改变结构时,脱轨的行为终将回落到最小阻力之路上。结构决定行为,而不是相反。
人饿了就会想吃东西,这是自然的结构决定的,但如果正在减肥,就会尝试控制食欲,这是对自我未来的渴望的结构决定的。两难的抉择导致了结构性冲突的发生,少吃身体受不了,吃多了违背减肥的愿望。压抑进食的欲望可以舒缓减肥的压力,但被压抑的欲望终将爆发,这时就会暴饮暴食舒缓进食的压力,恶性循环之下整个系统就会处于不断的来回震荡之中。
当仅仅从表面上的结果看待自己的行为时,任何尝试在结构性冲突中解决问题的方式都可能导致结构进一步的固化,因为你在任一结构上施加的力量都会受到其他结构的反弹,最后带来深深地无力感。
这同时也是大部分人给的建议没用的原因,因为他们不知道你的行为根源于你所处的生活带来的潜在性结构中,而只是尝试从表面上矫正你的行为。
冲突的一部分根源在于我们渴望某事,但又被教导了大量的“常识”说很多事是不可能的。而渴望的压力借由某事的实现而舒缓,不可能的信念借由某事失败而舒缓。
渴望的事情一旦获得初步的成功就会开始积累不可能的常识系统中的压力,直到困难越来越多的时候,不可能系统中的压力成为了主导,借着放弃舒缓了压力,而这又积累了渴望系统中的压力,循环往复震荡不休。
面对这种情况,要么我们会止步不前,不敢改变现状,要么我们就把责任归咎于自己的意志力,尝试逼迫自己去做,更可怕的是恐吓自己,渲染如果不那么做就可能面对的种种悲惨的未来,借此推动自己行动。
以解决问题为目标的弊端
就算我们勇于面对问题,不断尝试应对挑战,却会发现问题越来越多。因为以解决问题为目标时,我们只有在问题恶化时才会采取行动。一旦行动改善了问题时,动力也就随之减弱了。但此时问题依旧存在,依旧可能再次恶化。以解决问题为导向的人生,终将精疲力尽而不知其所归处。
一受其成形,不忘以待尽。与物相刃相靡,其行尽如驰,而莫之能止,不亦悲乎!终身役役而不见其成功,苶然疲役而不知其所归,可不哀邪!人谓之不死,奚益?其形化,其心与之然,可不谓大哀乎!人之生也,固若是芒乎?其我独芒,而人亦有不芒者乎?— 《庄子·齐物论》
20220404165519_debby-hudson-bAYP_kAtNWg-unsplash-tuya
有解的人生
那我们该怎么办?既然结构决定行为,那我们尝试改变结构不就好了。潜藏的结构并非不可改变,我们可以通过改变生活的结构开始,创造新的结构,重新塑造行为的最小阻力之路。
这不仅更彻底,而且更好玩。解决问题是为了消灭某些东西,而创造恰恰相反,是为了孕育某些东西。解决问题通常解决不了根本问题,而是埋下了解决下一个问题的种子。我们应当把注意力放在如何创造想要的生活,而不是想要解决的问题和摆脱的东西上。创造新的结构就是创造新的世界,只有这样才能跳出原有的结构性冲突。正如治病和追求健康,采取的方式,努力的方向都完全不同,解决问题不可避免,但不要为了解决问题而解决问题。
问题无法被解决,只会被更大的问题掩盖掉。进一步仔细斟酌后,我们可以看出这种“更大的问题”将会把注意力提升到另一个层次。病人的兴趣提高,关注范围变大,而且视野变宽,无解的问题也就失去了急迫性。并非问题本身获得了合理的解决,而是因为一种更新更强烈的生活焦点出现,问题只是相形失色了。— 卡尔·荣格
创造
何谓创造?它并不是指问题创造性的解决方案,那无非是采取了一种新奇的方式,归根结底还是为了解决问题而服务。这里的创造更接近艺术家画画,艺术家画画本身并不是为了解决什么问题,而是为了创造新的作品。
创造不以解决问题为目的,创造不通过给自己施加压力和暗示来推动事情的发展,因为人就算处于负面状态中,也会想创造他想创造的东西,这是一种自然倾向,“人只能成为他想成为的人”。
创造要求如实面对自己,因为这是创造的起点,“因为某些事情看起来不可能,你就不愿意承认它们是你想要的,这才是对现实的扭曲”。
“领导者的角色就是去创造一个使得最理想情况最可能出现的环境” ,我们不正是我们人生的领导者吗?但如果我没有勇气,害怕了怎么办。人内心自然有创造的冲动,人也天然知道他想要的,甚至人不能不做他想要的,一旦背离了这一点,就否认了现实,陷入了痛苦的结构性冲突导致的循环振荡之中。
而且创造最美妙的地方不也正是在于未知吗?
最棒的那部分是你现在还不知道的。如果你全都了解了,那就不叫创造,而是听写了 — 葛楚德·史坦因
无知相比知识更多的乞求信心 — 达尔文
当我们说某些人是杠精的时候,我们到底想表达什么呢?
是说这些人不应当质疑我们视为理所当然的常识?还是说他们偏离了探讨的话题而转向了琐碎的细节?还是恼怒于他们竟敢反对我们的观点,难道他们不知道反对我的观点就等于反对我这个人,反对我奉为圭臬的信念吗?
亚里士多德说“哲学起于惊奇”,人为何而惊奇?不正是发现了这个世界不如常识所断言的那般无聊和庸常吗?不正是发现了很多斩钉截铁的结论是建立于摇摇欲坠的地基之上吗?不正是发现了很多人其实也不明白自己在说些什么,而他们竟还如此自信吗?正是人对这些问题的不安、无措、惊讶和困惑,引发了探求的冲动,把人引向了哲学活动。
哲学论证第一原则
哲学讨论允许任何看法,只要能够给出理由 — 罗森伯格
在哲学批判中,单纯表达对结论的反对除了情绪上的宣泄外并没有提供任何有价值的信息。有意义的结论来自正确的前提加有效的论证形式,批判必须具体的指出对方论证中哪一步出了错,所以批判的关键就在于话题的改变,深入对方的论证中寻找分歧之处。
当前提为真,且论证形式有效时,结论必定为真。所以分歧要么落在前提上,要么落在论证形式上。当我们质疑前提时,往往质疑的就是所谓的“常识”,而这恰巧是被预设不能质疑的对象。当我们质疑论证的形式时,又进入纯粹抽象的逻辑学领域,落入琐碎的细节中。更重要的是,很多人对结论的信心来自于他对自己的信心,而非论证,当你想要他给出论证时,就已经冒犯了他,因为往往他并不能给出什么论证,承认这一点是一种禁忌。
如何开始
千真万确,根本没有什么创造、发现、洞见、理解的技术是可以教授的。……你可以教人如何观看,但教不了如何看懂;可以教人如何探寻,但教不了如何发现 — 罗森伯格
信息可以被传达,理解不行。正如词典,只有在你已经理解词的真正含义,只是不了解对应的文字时才有意义。例如词典将“红”描述为鲜活的树叶的颜色,只有明了鲜活的树叶是怎样的人才能理解“红”的含义,否则就如同盲人一样,穷尽表达依旧无法使他获知“红”究竟代表着什么。从这个角度讲,对盲人来说,关于红色的任何讨论都是既不为真也不为假的,因为讨论本身脱离了跟盲人所有其他观念的连接。
所以依赖他人的文字了解哲学是危险的,因为解读中里找到的并不是意义本身,而只是更多的字词。这些词又是哪里来的呢?归根结底还是他人的理解,而他的理解又从何而来?无非是对方自己思考的结果,这不就是我们当前正在做的吗?最终对方解读的对不对依旧还是需要我们自己来判断。
如何结束
引起惊奇的问题,借着阅读和思考,可能得到解决,可能得到明晰,甚至可能更为令人困惑,但不管如何,最终的解答还是需要我们自己作出。
亚里士多德说“人是具有理性的动物”,又言说“沉思是最高的道德”,何谓沉思,那便是对理性的充分运用,只有此时人才成其为人。苏格拉底说“未经考察的人生不值一过” ,隐含之意便是“未经考察的人生(是动物的一生,)不值一过”。
(哲学),这是专属于人类的乐趣,这只为我们这种生活在言说与理性中的动物而备。不经过蜕变你就难以体验。说来也怪,……最终与我们最为相关的并不是日常世俗事务和谋生,而恰恰是自由与不受束缚的理性活动–因为独有我们才是循理的动物 — 罗森伯格
存在即被承认
28 February 2022
20220228214925_CiKv0r
没有他者就没有自我
传统的观点认为交流的目的在于心灵共享,但这就面临一个问题,个人的经验如何超越自我的界限而传达给他人,即交流如何可能的问题?
黑格尔给出了他的解答:存在即被承认。人并不具有所谓的内在性,人的自我是在与世界(他者)的交互中呈现的,承认的过程就是解释的过程。取消了人的内在性,也就解决了传统交流观带来的悖论。
自我是没有内在的——自我对自己的发现是在光天化日之下发生,是在他者的陪伴之下发生。如果你提出,你不可能知道我内在的想法和感觉,那就是对交流之必要性的侮辱 —— 《对空言说》
但这不过是将人与人交流之不可能转换到人与作为他者的世界之间,那自我和他者的交互又如何可能?
叔本华认为我们必须通过主体认识对象,对象借由被主体认识而显现自己,反过来也可以说通过对象才显现出主体。
因此他者即是自我,也可以说“我即是他者” ,世界不过是我的表象,我们跟世界之间的交流,不仅只是单向的理解,更是借此认识我们自己:生与死、存在与虚无、人生之意义所在。
少见的艺术家
23 January 2022
20220123171921_art
“命运掌握在你自己手中,但你的手却是那样软弱无力” —— 约瑟夫.康拉德,英国作家
《艺术与恐惧》的作者曾言:如果美国 97% 的医学生毕业后不再从事医学专业,民众和政府一定会要求介入调查,但艺术系同样的情况却没人大惊小怪。
他悲哀的说道“艺术家往往都是学生”,因为学生时代的艺术生涯就是他们作为艺术家的唯一时期。
但艺术生的辍学率并不高,他们放弃创作归根结底是大学艺术教育系统的失败,它们忘了培训学生如何靠艺术谋生,以及离校后不再有人关心和支持他们的创作时如何坚持下去。
“眺望远方是一回事,而走到那里却是另一回事” —— 布兰库西,罗马尼亚雕塑家
大学艺术教育本身的有用性也要打个折扣,毕竟这个领域最优秀的毕业生都自称是正规教育的幸存者。艺术家杰里·尤斯曼将指导研究生创作称为“修复因接受过多教育而造成的伤害”。
很多学生毕业后要么转行,要么攻读研究生尝试从事教职。这完全把艺术教育系统变成了一个传销网络,每年必须有新生进入艺术院校就读,艺术系的研究生才能找到教职。
这样的循环让艺术教育彻底成了一潭死水,学习艺术应该是为了创作才对,而不是领一份安稳的教学工资,这完全背离了艺术的探索精神。
创作,创作
对那些有志于从事艺术生涯的学生,作者并不讳言这条道路的艰难,他真诚的给出了自己的建议:
“才华是一种天赋,但创作永远是一种努力”
以前人们出于宗教、习俗等创作,但现今真理、确定性、信仰都已不复存在,创作者必须从自己的作品中汲取力量,在这个过程中他可能面临自我怀疑和恐惧,不确定性的阴云笼罩着他。但艺术创作必须要在创作中学习,只有大量创作,才能留下真正优秀的作品。
“优秀的作品能够表达真理 —— 于这个世界永远重要的东西”
但谨记不要让完美成为自己走向完美的阻碍,毕竟“至善者,善之敌”。
爱欲之死
1 January 2022
前言
昨天在 zenzen 偶然发现一本小书《爱欲之死》,之前读过弗洛姆《爱的艺术》,很好奇另一位哲学家对爱有什么看法,便拿来读了读,虽然不是很认同作者的观点,但还是激发了我的一些思考。
爱
人因其有限性和不确定性而有追寻无限性和确定性的倾向,用奥古斯丁的话说“人渴望被爱”。
在奥古斯丁看来,人对爱的渴求是永无止境、永不满足的,而人又常期望被爱的东西提供超出所能的爱,但只有上帝能提供无限的爱,对任何其他事物的追寻都将使人落入痛苦之中。
对不得不爱的人来说,必须先爱上帝才能爱世界。
但在弗洛姆看来,人本身的缺陷正是爱得以成立的基础,爱是可能性的艺术,是成熟的人之间生命的碰撞和交融。爱情是一支双人舞,它打破人的自我,让世界在新的视角重生。
相比弗洛姆对个体和自我的强调,《爱欲之死》中韩炳哲则认为,承认自己的无能,通过将自己完全奉献给他者,自我的消解才是爱生发的土壤。
“爱情中的双方必须首先从自我中走出来,走进对方,让自我在对方中死去才能重生” - 马尔西里奥·费奇诺
韩炳哲对重生的承诺来自黑格尔的辩证法: 正反题的矛盾导致合题,合题又成为新的正题,如此循环反复推动演变。
爱着的人在被爱的人身上忘却了自己,正是这种忘却使得他找回并重新拥有自己,这便是爱的馈赠。
但这被弗洛姆认为是一种病态的爱,因为它消灭了人的可能性,而它所承诺的重生遥不可及。
作为享受的爱
《爱欲之死》最精彩的论述在于对消费主义社会中作为消费品的爱的描述,也即弗洛姆的物质之爱。
当爱作为一种商品在消费主义社会中存在时,它便被约定满足人们的需求,即带来享受。
爱必须带来享受,带来连续不断的心理和生理刺激,它必须免于伤害、攻击等负面行为的影响,就算痛苦也必须以享受的方式存在。
然而“爱不是一种可能性,它并不基于我们的努力和积极态度而存在,它可以没来由地打击我们,伤害我们”,否定爱的消极性,也毁灭了爱。
当爱落入一种需求、消费、满足的模式,人们变得越来越相似,在他人身上也只是找寻和确认自己,甚至连他人的存在也无关紧要了。
就算这样的爱,在现代社会中也正在不断消亡,因为这样的爱和性还需要他者的存在,而色情作为单人舞,更容易被消费和满足。
“性不会毁于道德、高尚和压迫,而最有可能毁于比性更有代表性的东西——色情” —— Jean Baudrillard
the-death-of-love
爱的消亡
在《爱的艺术》的结尾,弗洛姆哀叹爱在西方文明中的衰亡,资本主义的高度发展将人变成了实现商品交换价值的手段,人被异化为一边拼命生产,一边拼命消费的怪物,取消了人的神圣性,也取消了爱之所以可能的土壤。《爱欲之死》同样将爱的消亡归于消费主义的发展,在消费主义中,人的自我不断扩展,自爱发展为自恋,作为爱中不可缺少的他者已经不复存在。
爱是一种能力
很遗憾,在这些人看来,爱是一种能力,不是什么人都可以爱,什么的都可以的那是欲。
奥古斯丁之先爱上帝,弗洛姆所谓成熟的个体以及韩炳哲强调的爱中不可或缺的他者,都不是随便可以达到的地步,而这仅仅是爱的土壤,但这才是很多人孜孜以求的爱不是吗?
“All things excellent are as difficult as they are rare” —— Spinoza
世界围绕着心灵旋转
4 August 2021
读西方哲学史有感:哲学在康德的时代走入了死胡同,理性主义者靠主观臆断来建立他们的哲学大厦,蔑视经验,而经验主义者更是陷于怀疑的深渊,否定却无力建立。
康德认为休谟打开了新世界的大门,但却留下一个最重要的问题没有回答: 即人类的知识,科学,是如何可能的问题。
尤其在科学不断取得迅猛发展的时代,这一问题变得越来越急迫,因为科学正反过来想将世界纳入它的机械宇宙之中。
而他的解决方法是如此的巧妙和具有开创性,以至于被人称之为哲学上的哥白尼革命。
康德认为世界围绕着心灵旋转,而不是相反。
“从整体上来说世界依赖于我们,正如我们在细节上依赖于世界一样” - 叔本华
我们相比被动的经验者,更是积极的创造者,而这带来了无穷的可能性。
以至于哲学此后被分为康德之前的哲学和康德之后的哲学。
日常之外的可能性
25 July 2021
生命的遗憾在于走向不可避免的死亡前只能经历一种人生。
而哲学启迪我们如何去探索日常之外的可能性。
我们的生活是无数种物理可能性之一,而物理现实又是无数种逻辑可能性之一。
“吾生也有涯而知也无涯,以有涯随无涯,殆己!”
只有沉思这一切的缘起,才能有所感悟。
正如庄子言“枢始得其环中,以应无穷“。
轻信
26 July 2021
小时候我很容易相信别人,当时想着别人没有理由骗我,就算我相信了对他们也没什么好处。
直到有一天我发现,原来大家都只是复述着不知道哪里来的信息而已,就像盲人抓着前面另一个盲人。
我猜轻信可能是远古残留的习惯,那时人只有感官经验可以信任,怀疑风吹草动中是否有猛兽会导致不幸的死亡。
自从人发明语言和抽象的概念之后,这种轻信便被滥用了。
当人装满了一堆不知何处而来的信念,犹如背着沉重包袱的奴隶,他却以此为自豪,再也不能放下这些去看新的世界。
“你还不自由;你还在寻找自由。寻找使你彻夜不眠,且过度地醒寤。你想升于自由底高空,你的灵魂渴欲星辰。” - 尼采
很久前就听过"维护良好的笔记系统是人的第二大脑"这种说法,但一直不甚在意,我基本上不记笔记,记了也不看,人生也很少因此苦恼。
当时认为笔记仅仅是用于存储信息,怎么比得上谷歌这种索引海量信息并实时更新排序的搜索引擎,真正重要的是一条够快的网线。
直到年前接触了卢曼卡片盒这一笔记思想,通过半年的时间积累了大概800多张卡片,累计十万余字的手打笔记,才发现第二大脑的真正含义。
如果你有一个朋友
记忆超群,记得所有你认为真正重要的东西
他的智力是你的上限,终日思索着那些重要事物的内在本质和联系,不为琐碎的细节所累
精力充沛,随时能用你能听懂的话回答你的问题
终身陪伴,不离不弃
你觉得你的人生会发生什么样的变化呢?
10 May 2021
「元思笔记」缘起
陆陆续续接触过很多笔记软件,包括 Evernote、Marginote等,但一直没养成记笔记的习惯。
年前接触到了大火的 Roam Research,其支持的块引用,双向链接等功能用来管理一些信息蛮方便的。就对启发了作者的卢曼卡片盒笔记方法产生了好奇,进而追溯到了介绍该方法的书籍《How to Take Smart Notes: One Simple Technique to Boost Writing, Learning and Thinking》。正值过年在家,果断开始读起。
在阅读中发现 Roam Research 其实并不是纯粹的卢曼卡片盒笔记软件,只是吸取了其一部分理念。而原版的卢曼卡片盒笔记方法更对我的胃口,经过一番查找,发现手机端并没有比较好的对应软件,就想着能不能自己开发一款。这便是「元思笔记」的起源。
20210510183139_HowtoTakeSmartNotes
卢曼卡片盒
该笔记方法致力于构建自生长的知识网络,来辅助思考和输出。由此总结的几条原则
Garbage in,garbage out
慎重对待你将要记录的信息,评估是否将其纳入你永久的知识网络之中。
重述而不是记录
所有的笔记都应该经过自己的重述,而不是单纯的记录。
原子化
以卡片为单位记录笔记,每张卡片应当包括足够的信息以使其可以被单独理解。
创造关联
尽可能的创造不同卡片之间的关联,并补充对应的上下文。
20210510183611_2
“汲取日常中的奥秘,吐露冰雪般的凝思”
综上原则,元思选择了特罗斯特朗姆诗集《沉石与火舌》译者对其诗句的评价:“汲取日常中的奥秘,吐露冰雪般的凝思”作为 slogan。正贴合元思构建卡片的理念。
下面是正式介绍:
元思笔记 - 打造自生长的知识网络
元思笔记是基于卢曼卡片盒而开发的极简笔记软件。
通过创建知识卡片以及维护它们之间的关系来构建自生长的知识网络。
本地保存内容,保证隐私安全
支持随时导出导入备份
支持双向链接以及父子关系关联
支持简略 Markdown 格式
20210510183420_0-1
20210510183423_1
20210510183425_2
20210510183427_3
尾声
过年期间元思基本功能就已经开发的差不多了,到五一抽空整理上架,中间自己试用了两个多月的时间。总共积累了 574 张卡片,上千条卡片之间的关联关系,约五万字的纯手打笔记。相比之前自己的笔记方法可以说是一个飞跃。
目前安卓版本已经上架了,iOS 正在审核中。后续准备开发基于 webdav 的云备份和桌面端。感兴趣的朋友欢迎下载体验,也欢迎对笔记方法感兴趣的入群交流。
元思笔记官网: https://metaslip.com
iOS 已上架: https://apps.apple.com/cn/app/…
附录
详细介绍卢曼笔记盒的一篇非常不错的译文:卡片盒笔记法详细介绍
之前参加 神兽之间 书店《沉石与火舌》读书会的会后分享: 阅读之初是被诗人凝练意象所构建的画面吸引。在凝视的过程中,这画面竟借着时空的涌动而缓缓展开,在静与动的转换中一个意象钩沉着一个意象,直到将整个人都拉入诗中,陷入深深的共鸣。
食灯鬼大佬,有个feature请考虑下。 搜索结果,当一个卡片里命中多个关键词,现在的结果列表展现不是很好。(甚至其实根本搜不出来多个关键词)。具体说,目前的搜索列表页显示的上下文太短,当卡片量比较大的时候,很难通过这么一小句,快速做出准确定位卡片,得一个一个点进去看。 另外更致命的是…想搜索到的那个词,所在的那句话话,并没有搜出来…。。
或许可以借鉴一下flomo的设计,把搜索列表页直接全文展开,高亮每一个搜索词,用户就可以一目了然了。
因为卡片并不长,展开没什么问题。
感谢!
shidenggui
MOD
4 May 2022 10:54
多谢建议,搜索这块在卡片量上来后确实不太方便,后面我会再想一想怎么设计比较好
shidenggui
MOD
1 June 2022 19:21
最新版本已经实现多关键字搜索和展开全文
Reply
Reply
Reply
249
26 April 2022 11:50
您好!感谢您的笔记应用,我刚开始使用元思笔记,请问几个问题:1、是否没有windows系统的元思笔记了?我之前下的windows版本,点击确认无法添加卡片,官网也已经没有windows选项;2、ios版本在用蓝牙键盘输入时,打错拼音退格之后输入法待选框里错误输入的内容并不消失,只能无数次先选择错误的字再删除;3、光标的移动很迟钝,不能跨行选择文字。后两者在其他app没有出现,是否我使用方法的不对?
shidenggui
MOD
26 April 2022 14:15
目前没有 Windows 版本,之前的 beta 不推荐使用 第二和第三点都是底层使用的开发框架 flutter 的问题,目前只能等待框架完善
Reply
Reply
一双白手
2 April 2022 12:20
如果能添加图片和附件的话就完美了。MacBook版本编辑文字删除的时候光标会跳转到段首,不知道是输入法的问题还是笔记的问题。期待修复。弱弱的问下能拉我进下微信群么
Reply
一双白手
30 March 2022 17:54
元思笔记用着很不错,感谢提供这么好的产品,期待产品能做大做强。
Reply
Leo
2 March 2022 13:05
对您免费提供的产品表示非常感谢
Reply
ryenxx
12 January 2022 13:55
挺好的一个产品,期待有机会能分享西方哲学相关的笔记。
聪明的学习胜过大量时间的投入
7 June 2020
缘起
学习时间的投入固然重要,但是方法论的提升也不能忽视,所以我偶尔也会看看提升学习效率方面的资料,磨刀不误砍柴工。聪明的学习胜过大量时间的投入,这是Marty Lobdell在 Study Less Study Smart 这个讲座中的主题。
恰当的休息是长时间学习的效率保证
讲者的一个学生因为连续挂科下定决心好好学习,给自己规定了严苛的学习计划,每天连续学习七八个小时,但是几个月后的考试反而考的更差。
恰当的休息是聪明学习的第一个要点,因为随着学习时间的增长,疲倦后效率会逐渐下降。此时只需几分钟的休息,就可以让你的大脑重回巅峰。推荐一开始学习三十分钟,休息五分钟。随着你逐渐习惯这种方式,可以慢慢延长专注的时间。听起来是不是很像番茄工作法。
专用的学习空间,事半功倍
人的潜意识无时无刻不受环境的影响。当老师在课堂提问,而你有疑惑时会怎么做?下意识的举手。学习也是如此,很多人在餐厅、床上学习,这时环境因素会在潜意识作祟导致无法专注。我之前的文章 读《Why We Sleep》,也建议床作为睡觉的专用设施,倡导不睡觉就起床,以提高睡眠质量。
长此以往,你会培养起进入学习空间就能专注学习的习惯。很多人说自己学习的时候心不静,就是差这一点习惯的培养。
课后记笔记:先吸收,再消化
有时在课堂上刷刷的做笔记,复习时看笔记感觉都懂了,但考试却回忆不出,是因为人善于识别胜于回忆。人类进化出了异常发达的视觉系统,只需要一些残留的片段就可以解析出大量信息,给人已经掌握的错觉。
学习时专心吸收,之后花额外的时间不看资料并总结成笔记是更好的方式。一方面确认自己到底真正吸收了什么,查漏补缺,一方面事后总结可以强化对知识的记忆。
学习小组的重要性
人是社会动物,小组学习可以激发人的学习动力,同时适当的交流可以提高效率,以防闭门造车。而且相比个人,作为群体更容易长期坚持。我最近参加了一个朋友组织的线上自习室,整体效率确实有所提升。
带着问题去学习
泛泛的学习很难在你的大脑中留下什么印记。作者倡导先略读要学的内容,提出一些自己感兴趣的问题,然后带着问题寻求解答。提问让你想的更多,理解的更好。这跟《如何阅读一本书》的方法有异曲同工之妙,先读大纲,然后略读,最后精读。
教别人很重要
对于一些你自己也没有完全掌握的知识点,如果有机会教别人,不要错过。教学强迫你将知识以另外一种方式输出,检验你的掌握程度,同时为其打造了更多联结。
用进废退:知识通过联结而巩固
大脑作为网状结构,单纯的知识只是一些孤立的节点,如果不能跟其他信息产生交互,很快就会被遗忘以释放空间。所以在学习对应的知识点时,要尽量多联想、多实践。
用自己的话阐述:学习不是背诵
Reddit 有个很火的节点 Explain Like I’m Five,里面的人会提出各种各样的问题,答者假设对方的只有五岁,尝试将问题阐释清楚。能漂亮回答这些问题的人对知识的掌握必然到了一定层次。
尾声
视频内容类似之前看过的一本小书 《Learn More, Study Less》,介绍一些学术研究方面对提高学习效率有帮助的技巧,很多是老生常谈,不过实践才是最重要的不是吗?
经济机器是如何运行的?
6 June 2020
缘起
6 June 2020 _FnJT50
《经济机器是如何运行的》是全球最大私募基金桥水创始人 Ray Dalio 关于宏观经济的视频,时长三十分钟,简明介绍了推动经济运行的几个核心概念。
长期看,生产效率是唯一决定因素
5 June 2020 _DRW80i
生产效率的提升是经济长期发展的根本推动力。在不存在信贷的社会,只有提高生产效率,生产更多商品才能换取更多收入。社会的生产效率会随着科技进步缓慢上升,接近于一条直线。
5 June 2020 _y2s9xs
信贷对经济的推动
但现代社会为了推动经济发展可以依靠信用凭空创造货币,央行就是执行这一功能的机构。开始投放信贷后,个人可以依自己的信用从银行借入货币进行投资或者消费,而“你的支出就是他人的收入”,收入增长后可以购入更多资产(房产、股票等)推动其价格上涨,而资产价格上涨和收入增加又使得借贷人的信用增加可以借入更多的货币,最后形成正反馈使得货币不断扩张,资产价格不断上涨,引发通货膨胀,
5 June 2020 _MLbI1f
还不起债了
正反馈什么时候才会被打破呢?借债终归是要偿还的。当借入的货币越来越多,货币的边际投资回报率越来越低,直到其不足以支撑偿还时,紧缩就发生了。大家为了偿还债务,只能缩减支出,而你的支出就是他人的收入,大家收入减少,为了还债只能抛售资产,资产价格的降低进一步导致情况恶化,此时负向的正反馈形成,资产价格不断下跌,经济萧条,通货紧缩,最后引发衰退。2020-06-05_HE9tZu
怎么办?降息放水
当经济衰退时,央行为了刺激经济活动,开始降低贷款利率,缓解借贷人的债务负担,借贷人偿还借贷后,信用回升,又可以借入更多货币,新一轮循环发生了。通常短债务周期以五到八年不断轮回。
5 June 2020 _sRRowC
无限制印钞不行吗?
正反馈被打破的根源是借贷人无法产生足够的收益以偿还债务。那央行是否可以无限制发放货币给借贷人呢?这就牵扯到通货膨胀的问题,商品的价格 = 货币总量 / 商品总量,当生产效率没有提升时,无限制发放货币会导致商品价格上涨,带来恶性通货膨胀,引发动荡。民国的金圆券、津巴布韦的钱不如纸就是滥发的下场。
6 June 2020 _0wZQcJ
零利率、负利率之后怎么办?
虽然央行可以通过降低利率走出经济衰退,不过人类天性中的享乐和短视本性还是会导致社会整体负债率越来越高,直到再一次经济危机发生时,利率已经降到了零甚至负,央行失去了刺激经济活动的能力。这时候痛苦的去杠杆过程就来了,在这个过程中,大量企业破产,人员失业,社会经济活动萧条。
5 June 2020 _2pEKcP
此时政府通常有以下可选的方式:
削减支出,缩紧裤腰带过日子。
债务减记或者重组,少还或者延长还款时间。
转移支付,对富人征税。
5 June 2020 _OijpUv
削减支出和债务重组因为你的支出就是他人的收入,虽然债务少了,但大家收入可能降低的更多。
对富人征税也不是一个好主意,因为富人通常拥有更多的资源,也更聪明,他们一旦拒绝或者逃避征税,很可能引发民间的仇富心理,导致对抗甚至引发动荡。
而政府还有最后一招,就是印钞,这虽然是最无痛的方式,但是一旦超发过量货币导致恶性通货膨胀,会动摇整个社会的基础。美国在大萧条时期和次贷危机都大量印钞以缓解经济危机。
5 June 2020 _nUHjla
恰当的混合以上方式可以达成优雅的去杠杆,使经济在保证稳定的情况下逐渐恢复正常,而一旦失败结局就是美国的大萧条以及日本失去的二十年,更可怕的是国家之间尝试通过战争来摆脱危机,导致类似二战的情况。
通常长债务周期长达六七十年,包含五十多年的上杠杆期,两到三年的萧条期以及七到十年的通货膨胀复苏期。
6 June 2020 _VFBEQx
总结
生产力、短债务周期、长债务周期三种力量共同作用的经济曲线图。
5 June 2020 _5Wbx1q
Ray Dalio 最后也对个人提出了几点建议
不要让你的负债超过你的收入,不然总要一天你会被债务压垮。
不要让你的收入过快上涨超过你的生产力,否则最后你会丧失竞争力。类似温水煮青蛙,想想之前国企大下岗、收费站裁员之后的人们境遇。
花一切手段提高你的生产力。因为长期来看,这是唯一的决定性因素。
当下
视频制作于 2008 年次贷危机后不久,借助其中的框架,我们可以看看当下的情况如何。
因为疫情,美国利率在 08 年之后再一次来到了零点。
6 June 2020 _dP5TF7
而且开始了疯狂印钞模式,不知道会不会是下一次经济危机的开始。
6 June 2020 _cN43Uq
尾声
经济本身当然不可能简单的被这几种因素决定,但是框架建立了分析的基础,指明了最关键的因素影响。同样还有之前的股利贴现框架(DDM),假设当前资产价格由未来所有的收益折算到当下决定,就可以让我们在分析房价有更深刻的洞察,具体可见 从租售比看,北上深的房价为什么不贵?
Ray Dalio 还有一个《Principles For Success》,同样三十分钟,讲解的是他个人对如何达到成功构建的一套框架体系,同样精彩,下次有机会跟大家分享。
从租售比看,北上深的房价为什么不贵?
3 June 2020
为什么说房价存在泡沫?
北上深的过高的租售比一直被认为是房价泡沫的指标之一。租售比是指每平米租金除以每平米房价,为方便取租金单位为年。通常认为合理的租售比在 17 年至 25 年之间。
据查 2019 年北上广深的租售比分别为 59.6、54.4、53.8、51.6,而美国市中心的平均租售比为 9.02。光从字面租售比看,国内房价确实远超平均水平,但是事实真的如此吗?
从租金上涨的角度看租售比
现在买入房子的人通常持有房子上涨的预期,而租金的上涨也是可以预期的。如果房子的租金年年翻倍的话,现在高价买入的房子是否就显得不那么贵了呢?
房价是由什么决定的?
如果我们把房子看成一项资产,仅关注其带来的租金收益,作为房子这项资产唯一的现金流来源,它的估值自然就取决于未来卖出前提供的房租之和加卖出时的价格。
金融学中有个资产定价模型,称为股利贴现模型(DDM),可以通过考察一项资产未来所有的现金流对其当前价格进行估值。下面简单介绍该模型并以此来考察北京的租售比是否过高。
贴现率
首先介绍下贴现这个概念,大家都知道 80 年代的万元户跟现在的万元户不是一个等级,也常听到”钱不值钱了“这个说法,这里隐含的意义是随着通货膨胀等因素,当下的钱在未来一段时间如果不能保持一定收益率增长的话,从购买力上将会越来越不值钱。
2020 年的 100 多万相当于 80 年代的一万元。如果有人未卜先知,在 80 年代投资一万元时,至少希望 40 年后能取得 100 多万的收益才算保本,这需要年平均收益率达到 12%。而贴现指的是将未来某个时间的现金以特定的比率折算成当下的现金。以上为例,我们可以认为 80 年的一万元在未来 40 年的年平均贴现率为 12%,即 81 年的一万一千二才相当于 80 年的一万元,以此类推。而贴现率还有一个紧密关系的概念叫做内部收益率(IRR),从未来看当下是折现,从当下看未来就是收益率了。
租售比跟什么有关?
而我们知道房子持有一年后的收益为当年的租金加卖出价格,而一年后的收益需要按一定的贴现率折现到当下。
当下的房价 = 下一年的租金 / (1 + 年贴现率) + 一年后的房价 / (1 + 年贴现率)
一年后的房价 = 第二年的租金 / (1 + 年贴现率) + 第二年的房价 / (1 + 年贴现率)
可以无穷递推直到
当下的房价 = 第一年的租金 / (1 + 年贴现率) + 第二年的租金 / ( (1 + 年贴现率) * (1 + 年贴现率)) + …. 第n年的租金 / (1 + 年贴现率)^n + 第n年的房价/ (1 + 年贴现率)^n
代入等比数列求和公式以及一些数学技巧,最后可得
当下的房价 = 第一年租金 / (年贴现率 - 年租金上涨率)
租售比 = 当下的房价 / 第一年的租金 = 1 / (年贴现率 - 年租金上涨率)
最终
租售比 = 1 / (年贴现率 - 年租金上涨率)
可以发现租售比只跟年贴现率和年租金上涨率有关,而年贴现率和年租金上涨率我们可以考察过去的资料确定。
为什么说北上深的房价不贵?
经查询资料,过去十年北京平均房租上涨 149.11%,年房租上涨 9.55%,代入当下租售比 59.6,可得
过去十年平均贴现率 = 1 / 租售比 + 年租金上涨率 = 1 / 59.6 + 9.55% = 11.23%
这个贴现率算是高还是低呢?经查中国过去十年名义 GDP 即实际 GDP 计入通货膨胀后,年均增长率也在 10% 左右,接近计算出来的贴现率。
国外低租售比的原因
如果国内的投资客到国外投资,中国作为全球少有过去几十年高速增长的国家,国外成熟市场很难拥有北京一样的房租平均增长率,
假设房租年平均增速为 5% 的房子,投资客给出的租售比 = 1 / (年贴现率 - 年租金上涨率) = 1 / (11.23% - 5%) = 16
如果房租完全不涨的话,则租售比 = 1 / (年贴现率 - 年租金上涨率) = 1 / (11.23% - 0%) = 8.9 ,接近美国市中心平均租售比 9.02,而查询资料可知 2018 年全美租金同比增长目前保持在 1.5% 的温和水平。
到此就可以得出结论,成熟市场合理的租售比其实是指那些已经度过高速发展阶段的市场的租售比,因为成熟市场的租金在可见的未来无法快速上涨,自然投资者也不愿意高价购入对应的房产。
而北上广过高的租售比来自于过去几十年的高租金年平均增长率,而能支撑高租金的高工资和高收入归根结底来源于过去几十年中国的经济的高速发展,也即 GDP 的增长。
高租售比可以长久吗?
至于未来过高的租售比是否能够维持,一方面跟国家的经济增长有关,一方面跟城市本身的发展有关,对于同样身在成熟市场,但依旧高速发展的大城市其租售比依旧不低,例如美国的旧金山为 53,洛杉矶为 38.58,纽约为 37.25。通常来说过高租售比会缓慢降低到合理水平,但是如果大家的预期一致逆转的话,可能会出现暴涨暴跌的情况。
结尾
最近在看《金融学二十五讲》,里面谈到金融学一个核心问题就是如何对资产进行估值,取得共识的定价可以方便资产的流动进而优化资源配置。而资产定价的方式之一就是基于资产未来的现金流定价,也即我们谈到的股利贴现模型(DDM),假设资产的当前价格由它未来所有可能的带来的收益之和贴现而来。
如果仅仅因为资产价格可能上涨的预期持有资产的话,一旦预期逆转,资产价格泡沫就会破裂。
附录
报告:北上广深房屋租金回报率均低于 2%
疯狂的房租:一位北京客的十年居住账本
数据揭秘:全美25城近四年间房租增长率变化
Price-to-Rent Ratio in the 50 Largest U.S. Cities – 2020 Edition
一次业务中的算法应用:动态规划、图、树
29 April 2020
缘起
我的编程知识大部分都是自学的,算法并不好。但是作为程序员的自尊心告诉我,算法很重要,所以后面看了《Data Structures and Algorithm Analysis in C:Second Edition》、《Algorithms》这几本书入门了算法。
b5KEKK
非常幸运的是这两本书都有国内的英文版,价格低廉,尤其《Data Structures and Algorithm Analysis in C》售价仅仅 30 元。
1EKO7O
简洁优雅的算法搭配精妙绝伦的数据结构,时常给人以一种美的享受。
但是在业务中,算法的应用并不多,就算遇到了,通常也有了比较好的封装。但是之前我曾遇到一个问题,就很巧的利用了我学到的算法知识,最后也取得了不错的效果。
翻译模块的优化
不久前公司准备优化性能,经过分析发现翻译模块的性能差强人意,所以准备从这个地方入手,而我正好被分配来处理这个问题。下面是这个问题的简化版本:
翻译模块的功能
翻译模块是将数据库表的字段在展示给用户时进行翻译。假设有如下表结构(表名、字段右侧为对应翻译,表结构纯为示例):
user: 用户
name: 姓名
father: Foreign Key of 「user」: 父亲,也是指向自己的一个外键
car: 汽车
brand: 品牌
owner: Foreign Key of 「user」: 拥有者,也是指向 「user」表的外键
翻译模块的接口函数 translate 需要实现的功能为(省略无关细节):
输入表名、字段名,根据预设翻译输出对应的翻译数组,最后通过指定分隔符连接,比如中文用”的“。
user, name => 用户, 姓名 => 用户的姓名
car, brand => 汽车, 品牌 => 汽车的品牌
支持外键无限级联扩展,包括反向。
user, father, name => 用户, 父亲, 姓名 => 用户的父亲的姓名
user, car, brand => 用户, 汽车, 品牌 => 用户的汽车的品牌
支持循环引用
car, owner, father, father, name => 汽车, 拥有者, 父亲, 父亲, 姓名 => 汽车的拥有者的父亲的父亲的姓名
支持自定义翻译
从上一个示例可以看出,字段过多时直接翻译会导致结果过长,影响用户体验,而实际预设最长有近 20 个字段,同时用户可以自定义字段,实际长度更长。
这个问题可以通过引入自定义翻译缓解。如果预先自定义翻译 [car, owner] => ”车辆所有人”,[father, father] => ”爷爷”,则 [car, owner, father, father, name] => 车辆所有人的爷爷的姓名,相比原来简短很多。
其他
通用翻译的共享
多语言、增删改查等本文无关细节
因为翻译一部分来源是机器翻译,字段有数十万而且请求频率非常高,所以对翻译速度和内存有一定要求。
自定义翻译的实现
简单分析后发现这是一个算法问题,问题描述如下:
翻译模块之算法
给定两个字符串数组 arr1 以及 arr 2,arr1 为待翻译数组,arr2 为自定义翻译数组。待翻译数组中的字符可用自定义翻译中的匹配字符串替代,给出使得翻译结果片段最少的输出结果,有多个时输出任一即可。示例如下:
给出 arr1 = A, B, C, D, E, F,arr2 = B-C,C-D-E, E-F-G
输入 [A, B],因为 arr2 无匹配,输出 [A, B],最少为 2。
输入[A, B, C],arr2 中的 [B-C] 可取代匹配子数组 [B-C],输出 [A, B-C],最少为 2。
输入 [A, B, C, D, E],用 [C-D-E] 取代比使用 [B-C] 取代更短,所以输出 [A, B, C-D-E],最少为 2。
输入 [B, C, D, E, F, G],用 [B-C, E-F-G] 取代比使用 [C-D-E] 取代更短,所以输出 [B-C, D, E-F-G],最少为 3。
解法
典型的动态规划,对应方程为:
F_min(i) = min(F_min(i - 1) + 1, F_min(j) + 1) (j 满足 arr1[j + 1],…,arr[i] 的片段在 arr2 中存在)
F_min(i) 指 arr1 到 i 下标为止时满足条件的最少片段数
F_min(j) + 1 是指如果存在满足 arr1[j + 1: i + 1]的可替代片段存在于 arr2,即可以基于此加 1
时间复杂度 O(N^2)
比如翻译如上的 A, B, C ,初始条件 F_min(-1) = 0; F_min(0) = [A] = 1
i = 1, F_min(1) = min(F_min(0) + 1, F_min(j) + 1),因为 arr2 中不存在 A-B,所以 j 不存在,F_min(1) = F_min(0) + 1 = 2
i = 2, F_min(2) = min(F_min(1) + 1, F_min(j) + 1),此时存在 j = 0 时,arr1[1 : 3] = [B, C] 的片段 B-C 在 arr2 中存在,所以 F_min(2) = min(F_min(1) + 1, F_min(0) + 1) = min(2 + 1, 1 + 1) = 2
最少片段数为 2,实际的翻译结果为 A, B-C
代码
完整见 Github
def find(fields, seqs):
cost = [0] * (len(fields) + 1)
path = [None] * len(fields)
for i in range(len(fields)):
cost[i] = cost[i - 1] + 1
for j in range(i - 1, -1, -1):
if tuple(fields[j: i + 1]) not in seqs:
continue
if cost[j - 1] + 1 < cost[i]:
cost[i] = cost[j - 1] + 1
path[i] = j
return cost, path
fields = (‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’)
custom = {
(‘A’, ‘B’),
(‘B’, ‘C’),
(‘C’, ‘D’, ‘E’),
(‘E’, ‘F’),
}
find(fields, custom)
翻译模块之数据结构
但是上面的算法中,自定义翻译的存储方式比较浪费空间,(‘A’, ‘B’)和 (‘A’, ‘B’, ‘C’) 里的 A, B 重复存储了,而且查找操作也比较低效,这里自然想到用树来保存自定义翻译。同时在实际场景中,为了实现共享翻译功能,选择基于图构建表结构以及字段之间的关系。
一开始会读取表结构以及翻译数据在内存中构建对应的翻译图,最后数据结构如图所示:
29 April 2020 _XqLWHH
正方形代表图中的表节点
黑色表示常规表节点
黄色表示共享翻译节点
椭圆形表示图中的字段节点
蓝色为普通字段
紫色为外键字段
绿色矩形为翻译树的节点
结果
上线后结果不错,性能大幅提升,翻译速度从原来的平均每次 1ms,下降到 10us 级别,而内存因为使用图避免了很多不必要的重复创建,下降为原来的四分之一。
尾声
平常更多的是处理业务问题,最常用算法的地方就是在 LeetCode 了,难得在实际场景中应用一番,而且体验还不错,希望之后在实践中能遇到更多有意思的算法问题。
Shidenggui
作品
归档
订阅
现在还有必要拥有独立博客吗?谈谈我的独立博客史
22 February 2020
缘起
一直想拥有一个属于自己的独立博客,这或许算是我作为程序员的执念之一吧。
为什么
因为我从别人的博客中收益良多。像学习 Vim 的时候拜读过酷壳的 简明 Vim 练级攻略,还包括谷歌搜索时经常接触的其他优秀文章,期待有一天我也可以通过文章来帮助别人。
希望认识更多志同道合的朋友,人生太短,世界太大,如果有一天因为博客而结缘,进而成为朋友,不是很美妙的事情吗。这几年我通过博客认识了好几位朋友,虽然不多,但是都保持着不错的关系,我也期待着别人通过我的博客认识我的那一天。
希望能有一个自留地,有些话想说,有些事想做,有些回忆想留下。而假手他人,总不如自己可靠。
为什么一定要独立博客?
不可以依赖公众号、CSDN 等博客系统吗?曾经的我也是这样想的,为什么一定要这么麻烦,重要的不是写文章这件事吗?是的,最重要的是开始写。我也曾经使用静态博客、Github Isuues,以及公众号的方式来托管文章,但是随着长期使用它们总有不尽如人意的地方,后面会在我的独立博客史中谈到。核心的问题在于:我们并不能真正享有我们文章的所有权。
我的独立博客史
第一个博客,大学时代的初次尝试:WordPress
我在学生时代早期一直有记日记的习惯,然而到了高中后期因为学业的原因放弃了,除了不久前又尝试记录过一段时间,从此再也没有捡起来过。
到了七八年前的大学时代,希望能有一个地方记录我生活的念头再次燃起,这时候瞄准了功能强大的 WordPress,使用了当时新潮的 .me 域名,尤记得我的服务器是申请了 Github 的教育优惠,用领的 100 美金代金卷购买的 DigitalOcean 最便宜的 5 美刀一月主机,好不容易搭建上线之后发现因为外网的缘故,国内访问非常卡,而且 WordPress 本身非常臃肿,体验并不好,在断断续续写了几篇文章后就废弃了。
第二个博客,毕业后再起航:Jekyll 静态博客
时间荏苒,大学很快结束了,我于 2015 年 6 月底毕业,工作之后接触的多了,再次希望能有一个地方呼吸除了工作之外的清新空气。
第二个博客吸取了教训,为了专注写文章这件事情,选择了 Jekyll 这种静态博客,托管于 Github,于 2015 年 8 月 17 日上线。在这里第一次认识到 Markdown 文件格式,后面一直基于此编写文章。博客上线初期写了几篇文章,后面因为系统故障,本地原始文件丢失,然后才发现 Jeklly 竟然只保存了渲染后的静态 html。
原始文件的丢失加换工作的缘故,进入了 9106 的高强度模式,情绪低落就没有继续写了。而博客也随着 .me 域名的到期而关闭,只留下 Github 的仓库遗迹。
jFDUxi
第三个博客,不死心之再出击: Github Issues
正所谓念念不忘,必有回响。在第二个博客关闭的两年半后,2018年 3 月5 日,我选择了 Github 的 Issues 继续我的博客之旅。
经过两次失败的沉痛反思和近五年的时光,我认识到不管使用如何花哨的技术,如果我不能保证持续的产出文章的话,我的独立博客之旅就要在此终结。我不甘心啊!
我告诉自己,这次我一定可以成功,因为我的独立博客之魂还在燃烧。而最重要的就是专注写文章,专注写文章,专注写文章。
这也是我选择 Github Issues 的方式,简洁、无广告,同时把 Blog 仓库置顶到首页,作为一个每天都上 Github 的来说,也是对自己的一个提醒和激励。
S6MX9w
这次我成功了,在花了一年多写了十来篇博客后,虽然产量不高,但是我发现坚持写文章对我来说已经不是问题了(如何坚持有机会额外分享)。而这时 Github Issues 的缺点也渐渐暴露了出来。
贴图困难。我之前都是手动截图,传到 Github 仓库,然后链接原始文件地址,非常繁琐。后面贴图的问题通过 upic 这个小工具有所缓解
分享困难。我有些读书笔记想和我的朋友们分享,但不是所有人都是程序员,访问 Github 对某些人来说太不方便了。
交流困难。因为分享困难,导致我的文章基本没有互动,除了第一篇文章有 10 来条捧场评论,后面在两年间通常只有一到两条评论。
没有统计。对于文章的相关数据基本一无所知,再加上评论的稀少,给自己一种玩单机游戏的感觉。
没有订阅。虽然有 Watch,但是功能还是太弱了,基本上可以说是可有可无。
第四个博客,有苦有乐:公众号
这时候我了解到了公众号这个平台,觉得对以上几个问题是比较好的解决方式。一开始是在 Github 发布文章之后再手动同步到公众号,但是很快我就喜欢上了公众号的体验。
zSEWiR
文章在公众号的阅读量和活跃度比在 Github Issues 高的多,而且跟朋友的交流也很方便,附带的数据统计也够用。这时因为贴图过于繁琐的问题,我渐渐不再同步内容到 Github Issues 上了。
在公众号写作了快一年后,公众号的缺陷也慢慢暴露出来了。
更新文章受限。文章发出后,只能有一次机会修改二十五个字。有时候只能通过留言来纠正,体验并不好。
广告嫌疑太浓。公众号目前的营销和商业氛围太浓。有时候跟朋友或者在群里聊天,都不敢发自己的公众号文章链接,总感觉自己是在打广告,特别羞耻。
无法被搜索引擎收录。文章的阅读数集中在发表后的一两天内,之后基本不再增加,除了特意翻看历史文章的人,基本不再有机会被曝光。
无法外链。之前我经常外链 Wiki、Github 等丰富文章内容,而公众号只提供一个阅读原文,有时候真的让人感觉非常不便。
自定义功能太弱。比如我想跟朋友交换友链、加上作品列表、支持 RSS 订阅、放上 Github 链接、这在公众号的系统中是无法满足的。
第五个博客,浴火重生:自己开发 shidenggui.com
其实很早前就动过开发博客系统的念头,本身作为一个后端工程师,博客系统可说是非常简单的应用。但是真正的问题在于前端,之前也曾自学过 CSS 和 Javascript,但 CSS 给我留下了难以掌握的心理阴影,尤其是其中的 Float 布局。
不过最近一年事情出现了转机,我花了不少时间打磨我的小说推荐网站 推书君(www.tuishujun.com) 以及对应的小程序版本,在这个过程中我渐渐熟悉了前端,虽然还称不上专业,但是我想开发一个简单的博客应该是够了。
时机成熟了,我告诉自己,而且正逢春节在家隔离,时间充裕。说动就动,在吸取了前几个博客的教训之后,做了以下几点考虑:
文章依旧使用 Markdown 编写,显示的时候渲染为 HTML。现在有很多优秀的 Markdown 编辑器,比如 typora 等,没有必要再实现一套复杂的编辑系统。而文章的元信息比如创建时间等参考了 hexo ,使用 YAML 附加于文章头部。
支持搜索引擎收录。使用支持服务端渲染的 SSR 框架 Nextjs。
保证加载速度。CSS 使用 Tailwincss,基本没有单独写过样式类,CSS 大小控制在 5K 以下。尽量减少 Javascript 类库的引入。使用 CDN 进行加速。
提供 RSS 和公众号两种订阅方式。
尽量小的内存占用,降低后续维护成本。使用 Docker 打包,整个博客包括使用 Nestjs 的后端,内存占用 170 MB。
总体开发花了三天时间,其中一天用于将公众号和 Github 的文章改为当前的 Markdown 格式,于 2020 年 2 月 14 日正式上线。
独立博客上线一周了,带来了什么变化?
说实话,效果有点超出我的预料,容我细细道来。
友链
博客上线之后,第一步自然就是跟朋友交换友链了,终于给朋友们的博客地址找了一个好家。
shidenggui.com_links(iPhoneX)
更好的文章分享支持
在发推或者 tg 群里面聊天的时候,常常会看到别人贴的链接有特效,而自己贴的文章却不行,研究后发现是需要在网页中添加关于社会化分享的配置,这次也给自己的博客加上了。
f0gjam
更好的传播
程序员的交流还有很多一部分不处于微信生态圈,而大家也大多不愿意分享微信文章链接。
博客上线一周之后,我遇到了一个针对开源开发者的福利活动,整体流程比较复杂,有不少朋友遇到问题,我就写了一篇自己的操作流程分享到博客上,看能不能对大家提供帮助。没想到大家非常热情,也有不少朋友写文章的时候顺带提了一句,结果这篇文章在三天不到的时间收获了两万多的阅读。
fuwEnm
虽然是暂时的,不过还是很高兴能给那么多人带来帮助,也算是给自己的博客开了一个好头。
一些小问题
图床:图片的存储一直是独立博客比较烦恼的问题,我这次使用的是 gitee 的图床,当后来发现图片无法在微信浏览器中正常加载,比较恼人,后续准备换用自己的存储系统,具体还在调研中。
原创保护:之前我在公众号翻译过一篇谈产品的文章,反响比较热烈,好几个人找我开转载白名单。而这次发布的教程文章同样受到好评,但是我在一天后发现 segmentfault 和知乎已经有了我的盗文,而我却没什么太好的办法。
尾声
博客刚刚上线一周,有很多小细节还在优化中,有些功能尚待开发,不过也收获了很多惊喜,后面我也会持续努力。
以一段话纪念我这曲折的独立博客史:
天方国古有神鸟,名「菲尼克斯」(Phoenix),满五百岁后,集香木自焚,复从死灰中更生,鲜美异常,不再死。 ————《凤凰涅槃》
后续更新
文章发表之后,有不少对独立博客感兴趣的朋友加我,因此建了微信群和 telegram 群。
telegram 群: 点我加群
想加入微信群的朋友,欢迎加我好友(博客右上角订阅/公众号菜单),进群一起交流。
Shidenggui
作品
归档
订阅
如何应对软件复杂度
22 January 2020
缘起
至今也写了四年多代码,但如何在功能不断增多的同时写出可读、可维护、可扩展的或者说优雅的代码一直令我感到非常困惑。
最近读了《Clean Architecture》以及 Domain Driven Design(简称 DDD) 相关软件架构的书,对这个问题有了进一步的思考。
战术的成功不代表战略的成功
虽然读过《代码大全》、《Clean Code》、《重构》、《Design Patterns》等书,也在编程中不断实践,但逐渐发现书中的技巧更着重细节的打磨,关注点在于如何把一个函数、类、模块设计好。不过大量模块集成后的系统质量却难以保证。
没有设计导向糟糕设计
为什么不是“没有设计就是糟糕设计”。因为当软件的复杂度很低时,无论如何都可以达到不错的质量。对于一个小模块,只需几个类就可以实现预期的功能。
但需求永无止境,功能堆叠难以避免。新功能可能是基于原模块修改,添加一些新类和新函数,也可能是通过新模块实现。此时模块、类、函数之间的交互逐渐复杂,虽然整个系统还可以理解,但是混乱的种子已经埋下。
cYlmsh
长此以往,人员的来来往往伴随着千人千面的设计,整个系统难以避免的走向了大泥球。
n5AUMn
失控的软件复杂度让“软件”变为“硬件”
大泥球的代码依旧可以运行,依旧可以为用户提供功能,依旧可以在上面修修补补,为什么这会成为一个问题呢?
因为现实中的需求和业务总是在不断的变化中,这也要求软件不断的发生变化以适应需求的变化,而失控的代码恰恰导致变化的成本非常高昂。
复杂的依赖关系意味着每一行代码修改的结果都会如同涟漪一般扩散到依赖的代码,导致难以预料的后果。
边界含糊的模块、类、函数互相交织,再没人能理解整个系统。缺失的上下文导致变更时,程序员只能从代码中重建对应的逻辑,往往落入盲人摸象的困境,而基于不完整理解的产物通常是四不像的怪物。
长此以往,变更、维护的成本会越来越高,软件易修改、可扩展的优点丧失殆尽,退化为“硬件”。
知道大泥球是避免大泥球的第一步
在大泥球中工作绝对不是一件愉快的事情,接手过旧有代码的人都可以理解,因为大泥球还有另外一个俗称“屎山”。从个人来说,为了愉快的编码,肯定不希望自己的代码变成大泥球。对公司来讲,代码的可扩展性、稳定性、可维护性是公司竞争力的来源,如果你可以比对手花更低的成本,更好更快的响应用户需求,自然能步步争先。
为我们的问题构建正确的心智模型
软件经过几十年的发展,能做的事情越来越多,复杂度越来越高。在这个过程中,大家花了很多的时间去思考怎么样才能更好的管理复杂度,因为软件的极限在于我们能多大程度上理解我们所创建的系统。
面向对象、垃圾回收、函数式编程、设计模式等都是思考的结晶,但它们更着重于技术的实现,随着软件不断整合复杂的现实世界,我们需要从更广阔的角度、用更好的方式来建模业务逻辑,DDD 即是其中流行的架构范式之一。
以技术为中心的架构设计
以技术为中心的思考方式通常能以最快的速度搞定需求。当开发一个功能时,首先考虑的是数据库怎么设计,接口怎么定义,需不需要缓存,如何利用框架已有的功能等,有了一个大概轮廓后,编码、测试、上线,不断循环。但从架构设计的角度,让代码跑起来只是第一步,如果新代码不能跟旧代码在整体设计上进行集成,最后很可能变成大泥球一般的四不像怪物。不过就算我们以良好的服务设计、模块化设计、类设计、接口设计将新功能优雅的整合进现有模块,以技术为中心的开发方式依旧有其最大的弊病。
技术为骨,业务为皮:研发为什么不说人话
在工作中,我常常听到客户经理、销售、产品对研发说:你为什么不说人话?因为通常来说,需求的发起方是来自销售、产品这些业务专家,他们对业务的建模(心智模型)通常跟我们以技术为中心的思考方式不同。他们并不了解微服务、关系型数据库、Mongodb、Redis、Route、Graphql、RESTful、gRPC 等技术术语,他们了解的是业务逻辑、业务对象及其交互。不懂技术的业务专家,不懂业务的技术,他们的沟通成果可想而知。
在实际的开发中,研发通常会将业务逻辑简称为 crud(增删改查),认为技术是骨,业务是皮,业务逻辑不过是一些简单、琐碎的编码过程,笑称专注于此的程序员为码农。长此以往,每个功能内含的底层业务逻辑被割裂,零碎的分散在整个代码库中。而新需求通常是基于业务专家的建模,此时谁还有能力从整个代码库中还原出整个业务蓝图并予以修改呢?
建模应当基于用户的心智模型
为什么要发明高级编程语言而不使用汇编?因为编程时通常不需要关心底层的寄存器、指令集。
为什么要发明 ORM 而不是用 SQL ?因为 SQL 作为关系型数据库,不是以面向对象的思考方式设计。
为什么要发明图形化界面而不使用命令行?因为大部分用户习惯通过图形化的方式来认知世界。
设计应当以终端用户的体验为中心,而架构说到底不过是设计的别名。当心智模型和底层逻辑匹配时,变更、维护、扩展的成本因此降低。而业务软件的用户是谁呢?业务专家以及熟悉该业务的用户。
t5V5eq
业务为骨,技术为皮: 领域驱动设计(DDD)
基于以上的思考,一些人在 20 年前提出了领域驱动设计(Domain Driven Design),简称 DDD,试图扭转这一趋势,提倡以业务为中心的架构设计。
以 DDD 的进行架构设计,首先不是关注语言、数据库、框架、RESTful、gRPC 等,而是从梳理业务逻辑出发,通过与业务专家的深入讨论,构建符合业务逻辑的模型,再通过各种各样的技术实现支撑业务逻辑的建模。在这个过程中要严格分离业务逻辑和技术实现,因为通常业务逻辑和技术实现有不同的变化速率以及变化方向。
分而治之:识别核心领域并精炼通用语言
一个产品有人使用,必然是有其核心优势,优势所在即其核心领域。比如今日头条的核心优势不是资讯,不是爬虫,而是其推荐引擎。资讯、爬虫、甚至抖音的短视频都是基于其推荐引擎的次级领域。对于一个影评网站来说,核心领域则是其评分、评论机制。而用户系统,包括登录、注册、重置密码、头像等功能,只能算是一些通用领域。
这样的话,我们可以基于业务领域的重要性进行划分:
核心域(Core Domain):产品竞争力的来源和优势所在,例如头条的推荐引擎。
支撑域(Support Domain):实现核心域的辅助领域。例如资讯、短视频的管理,以及如何依赖推荐引擎进行分发。
通用领域(Generic Domain):例如用户管理。这里是各种框架、库、第三方服务大放异彩的地方。
每个领域应该保证其中的核心概念具有精确的定义,否则“名不正,则言不顺”。正如用户管理领域中的用户跟推荐系统中的用户是不同的概念,具有不同的属性和行为,各有其偏重。
下图以保单为例,我们在承保时,关注的是保单的价格和理赔条件,在审核时,可能关注的是承保人的健康状况,保单是否为本人签名,而理赔时则关注对应的赔偿金额。如果强行把不同阶段的逻辑都整合到一个保单对象,必然会导致逻辑繁杂、主次不分。
M4ZeA8
领域的建模需要业务专家和技术人员的深入沟通
DDD 专注业务复杂度,致力于将业务专家的心智模型建模为技术模型。这个过程需要业务专家和技术人员深入沟通、并肩协作,通过大量的信息传递,在不断的讨论、质疑、磨合中提炼最终的模型。建模是 DDD 最重要的实践过程,也是 DDD 最难以落地的困难之一:对业务感兴趣并喜欢深入钻研的程序员并不多。
这个问题随着团队人数的增长,会越来越严峻。对个人开发者来说,设计和开发都在一人之手,自然也不存在沟通的问题。对于小型团队,如果不能保证团队成员对产品感兴趣,只能通过文档、培训等来普及领域知识。随着领域知识的不断增长和复杂化,超过个人的承载能力后,划分不可避免。
B3RwDM
领域的建模是动态的
领域建模需要深入设计,有人会疑惑:“那是不是需要整体设计完后才能开始编码,这不是退回到瀑布流的开发模式?” 并不是。很多 DDD 的倡导者本身也是敏捷和极限编程的支持者。他们认为一开始就认为设计出完美、精确的领域模型是不现实的。随着业务的扩张收缩、产品开发方向的变化,我们会不断获得新的领域知识,领域建模在开发、沟通、实践过程中会被不断精炼、完善。旧概念的理解会越来越透彻,新概念会不断涌现,一些不相关的概念会被剥离,因此代码的渐进式重构不可避免。领域驱动设计(DDD)也意味着对业务领域的深入理解驱动着设计的不断变化。
后记
DDD 提倡我们要尊重现实世界的业务逻辑,尊重已有业务专家的经验,以业务为中心驱动架构设计,更好的为业务服务。
对我来说真有当头棒喝的效果,毕业这几年已经习惯从技术的角度看问题,相信技术万能论。“当你手上有一把锤子的时候,看所有的东西都是钉子”,有时候效果很好,有时候却一叶障目,但长此以往,目光越来越狭窄是必然的。
DDD 落地还有很多技术细节,有机会的话下次在谈。这里也欢迎喜欢编程、架构设计、DDD 的小伙伴一起来探讨。
Shidenggui
作品
归档
订阅
色彩的奥秘 | 设计杂谈(二)
30 December 2019
缘起
接 设计杂谈(一):什么是美?,当时买《美学浅谈 | 色彩学纲要》,以为是一本书。到手后才发现是由两本小书组成,不过在内容上也有所关联。之前作者在《美学浅谈》里面谈到美的感受分为心理和生理两方面,而色彩就是美的生理感受中不可或缺的一环。
edO83L
这本初版于民国年间的书是否已经过时了呢?
先给出答案吧,可以说大部分内容毫不过时。本书初版于1915 年左右,为吕澄先生从日本留学归来在上海教书期间所出,先生时年二十岁。虽然距今 100 多年,而且先生出书时也不过二十出头,但是书中屡屡出现光的折射、反射、细胞膜、光波、波长、振幅、视杆细胞、视锥细胞、地球大气、水蒸气等我们熟知的概念,甚至爱因斯坦的相对论也有所提及。
文中大部分理论通过 wiki 验证,百年后基本没有错误。民国时期的学者对世界潮流的把握和学识的广博真的非同一般。
色彩的起源?
既然是谈论色彩学,不免谈到色彩的起源,世界本无色彩一物。于物理,色彩只是光子的波动;于生理,不过是视网膜的刺激;于心理,全是人的主观感受。所以谈色彩,追根究底,免不了从物理的光学开始。
人肉眼可见的光的波长?
书中言及可见光的波长位于 400ns 至 760ns 之间,当前科学家测定一个典型的人通常对光波的感知范围在 390ns 至 740ns 之间。超出该范围的波长但觉其热,不见其光,名红外线(书中称赤外线),低于该范围的同理称紫外线。
7I7PSZ
这里有个细节,书中言波长都是称 0.0004 耗,经过对比才知道耗就是当今的毫米。
为什么看河边倒影,近处清晰,远处模糊?
同时看海,近处苍碧,远处但白茫茫一片。水体在反射光时,视折射的角度不同,对光有不同的吸收率,约接近九十度吸收率越低,所以人在近处看河边倒影时,近处视角接近九十度,反射的光量多,自然清晰。
书中还配了一副折射图,不过不像现在标识符都用 ABCD,而以非常有中国特色的甲乙丙丁代替。
SaAQVt
白光的组成?
牛顿通过经典的三棱镜分光实验发现白光本身由各种颜色的光混合而成。原理是光波本身因波长不同在对应的介质中有不同的折射率,因此当白光透过三棱镜时,原本混合的色光即分离开来。
8oUA04
为什么天是蓝的,日落时又显红色?
这个问题我也好奇过,至此才得到解答。首先要理解色光中,蓝光的波长较短,所以在通过大气层时,更容易跟空气中的尘埃、微粒发生碰撞导致散射。
iBlR5E
而在白天时,天空中都是散射的蓝光,所以导致整个天空都显蓝色。
tdB9If
日落时,太阳光平行地平线直射眼球,虽然蓝光的强度增加了,但是相比更加容易被散射的蓝光,波长更长的红光被散射的更少,因此夕阳常常有晚霞。
5hoyuc
色彩的表示(RGB 与 HSL)?
颜色本身是由各种色光混合而成,但是在使用颜色的时候,谈论 RGB 的配比不方便也过于抽象,所以常用另一说法,即色相、饱和度(saturation)或纯度(purity)、光度(luminosity)或明度(brightness)。
60me3k
色相即颜色,有赤橙黄绿青蓝紫之分,饱和度指色彩的纯度,越高即色彩越纯粹,而明度更多的是一个主观概念,虽然各种颜色的亮度相同,但是人对此的感觉却不同。
这里需要区分的是亮度(lightness)和明度(brightness),之前搜了下好像国内对此有各种争议,最后翻了下英文 wiki 搞清楚了,简述如下:
颜色的亮度是一个物理学概念,可以认为是该颜色接受到的光强度。而明度指的是这个颜色在人心理上的感受强度。因为同样光照强度下,人对不同的颜色会感知到不同的亮度。而颜色亮度和明度的差异被称为 Helmholtz–Kohlrausch effect 。
Helmholtz–Kohlrausch effect?
该效应可以简述为在同样的亮度下,某些颜色具有比其他颜色更亮的倾向。
EjpeM6
以上这张图片中,左侧所有颜色都为同一亮度,但是明显可感受到右上角的红、紫两色更加鲜艳,而左上角的黄色非常暗淡。右图是将左图转变为灰度图之后的效果。
而亮度和明度之比会随着颜色饱和度的增强而增强,最高可达两倍以上。
GII9TO
这也解释了为什么大家喜欢用红色表示警戒,因为在同样的亮度下,红色可以达到环境亮度两倍以上的明度,给人以更加强烈的视觉刺激。
而绿色本身的 hk 效应较弱,比较不容易刺激视觉,所以给人更舒缓的感受。
我认为这可能跟红色是血、火的颜色有关。血、火意味着危险,如果能提早发现的话,可以增加生存几率,所以在物种演化过程中,大脑对外界的视觉信号做了修饰,增强对红色的刺激。而绿色本身是随处可见的植物的颜色,对人的威胁很小,不需要过度刺激,所以增强效应很弱。
人是怎么发现颜色会因对比发生变化的?
书中谈及一个法国化学家在监督织物染色时,发现花纹染色的结果经常因地色而发生变化,才察觉对色彩的感知会因为颜色之间的对比而发生变化。以黑白相列,则黑越显其黑,白越显其白。此人后面研究该问题出版了《色彩调和及对比》一书。
饱和度和亮度有什么区别??
书上饱和度是颜色的纯粹程度,而亮度则是明暗,依旧还是太过抽象了。按照 CSS 中 HSL 的定义,饱和度指的是颜色含有多少灰色,而亮度是指颜色含有多少的黑色和白色。当亮度为 50% 时,颜色中黑色和白色达到了平衡。
gjQVTd
示例图中可见中间色条随着饱和度的降低只剩下灰色,为 100 则颜色最纯粹。右侧亮度为 0 时为黑色,为 100 时为白色。
物体颜色的来源?
光照射到物体上,一部分被吸收,一部分被反射。物体的明暗通常取决于物体反射的光量,颜色则取决于白光中某些色光被吸收之后残余的其他色光。
通常来说,呈现某种颜色物体并不仅仅只反射对应的色光,通常还会反射临近颜色的光波,只是主要反射的色光足以支配其他颜色,使得其他颜色好似不存在一般。而这种主要颜色即得名「支配色」(dominant hue)。
因为物体只反射一种色光的话,其他色光都被吸收会导致亮度不足,使得暗淡难以识别。
原色及白黑灰(无色系统)
原色即是我们熟悉的三原色 RGB 了,其他颜色皆由这三者混合而来。在物理上讲,白黑灰本身并不是颜色,而是各种色光混合、消失在心理生成的印象。
光源强弱对颜色识别的影响
随着亮度的降低,人首先不能识别黄蓝两色,次而不能识别红紫,最后剩下的绿色随着亮度的降低变为灰色。
fqt2cM
书中举例说天色薄暮时,仔细观察花圃中的红色花朵,可以看到它们次第变暗,但是它们的“叶色犹甚明绿而不改也”。
100 年前的人怎么传递颜色?
目前的设计稿中都会包含对应的 RGB 值。而民国时期的人没有电脑,他们通过什么方式来将颜色传递给制造工人,记录说用什么颜色呢?
JaSMdn
书里面介绍,直接用配出来的颜色卡纸传递,很容易褪色变形,所以他们通用的方法为圆盘配色法。即购买固定厂牌的色卡,在圆盘上记录对应赤橙黄绿青蓝紫的占比,使得圆盘旋转之后的颜色等于所配的颜色,此时只需要传递对应颜色的配比即可。
EtJX37
补色或余色(Complementary Color or Supplementary Color)
如果两种色光混合可得白光,则它们互为补色。就色环而言,相对的颜色就是补色。
mIMSFq
当两种补色临近时,会产生强烈的对比感。
zjeZd9
色光是怎么在我们的意识中产生对应的颜色?
前面谈到的都是色光的混合,那从物理的光波怎么变成我们看到的颜色呢?
这里就要谈到人的视网膜的构成,视网膜上分布着视杆细胞和视锥细胞。视杆细胞只能察觉明暗,而视锥细胞才能察觉颜色,所以我们的色彩基本都是源自视锥细胞。而视锥细胞有三种,分布对红、绿、蓝敏感,这也是三原色的由来。
Bly78V
残感:颜色的升起和消灭都需要时间
从视网膜细胞接受到光波产生电刺激,通过视神经到达大脑中枢产生颜色,期间需要一段时间。颜色的亮度越亮,则产生颜色所花的时间越短。
相对的,颜色的消失也需要时间,这被称为残感。残留的颜色与原来颜色一致的被称为同色残象,与原颜色相反的被称为补色残象。
为什么补色并列会产生强烈的对比?
这跟之前提到的补色残象有关。当直视黄色物体后,移视白色地面,会升起蓝色的补色残象,此时如果直视蓝色物体,叠加蓝色残象,“先后迭观,必更见其辉明”。
为什么会有补色残象?
之前提到视网膜上有感知红绿蓝三色的细胞,但是视网膜的冲动需要通过视神经传递到大脑,视神经却不是直接传递三色冲动,而是将其转化为明暗、红绿、黄蓝三个通道的比值。以黄蓝为例,黄蓝通道在受到黄光刺激时,视质分解产生黄色感觉,而移视它色时,视质补充性的合成产生蓝色感,如果此时直视蓝色,本身的视质合成叠加补充性合成的视质,蓝色自然显得更蓝了。该理论提出于 1828 年,梵高之类的画家都深受其影响。
r6Azgt
F7h0nX
色彩与情感
色彩能带来情绪的变化,有以下几种原因:
1、一般生物都有趋光性,因此人生而好白光而恶黑暗,因此有一切伴于光度之感情。
2、人生活好变化而恶单调,故有一切色相之变化。
3、色彩会跟我们人生经验中的情绪相结合,比如过年的喜庆红色。
4、一切光度色相之根本感情者,乃兴奋与沉静之感情。光度中越趋白则越感兴奋,越趋黑则越感沉静。色相中越趋红则越感兴奋热烈,越趋蓝则越感沉静,而绿色居于中间。
色彩之调和
色彩之配合以能生人愉快之感觉为美,此所谓调和也。色彩调和有以下几种:
1、色阶之调和:以同一色彩之各光度、饱和度相配合
lnKN0b
2、混合之调和:以光度相同之邻近色(黄与橙、绿与蓝)互相配合。
fe4qgQ
3、支配色光之调和:各种不同之色中,一色独见显著统属诸色。
I5MBvf
4、色相推移:保持同一光度和饱和度,在色环上以相同或不同的间距改变色相。
k2LOE4
后记
读书只花了五六个小时,写文章倒是花了十几个小时,主要时间花在考证书中的理论在一百年后的今天是否过时以及文章配图上。
这书本身是为美术(绘画)专业的学生所用的入门教材,书里面有不少篇幅的都是讨论颜料的配比以及如何做实验验证书中的理论。还专门有一小节讨论民国时期,在煤油灯偏黄的灯光下作画的注意事项。这里都略过不提。
书中谈配色的并不多,作者将配色放入了另一书《配色法》中详解。此书现在已经不可考究了。
没想到色彩中还蕴含着那么多奥秘,希望这篇文章对大家能有所帮助。
Shidenggui
作品
归档
订阅
人生第一次翻译英文文章的心得体会
18 December 2019
缘起
机缘巧合之下,看到一篇非常不错的谈创业和产品的文章,当时有位群友说要翻译,但苦于时间不够,我读完后觉得文章非常不错,就说由我来翻译。之后在群友的帮助和校对下,终于定稿,下面是在翻译过程中的体会:
我能读懂,应该可以很容易就翻译成中文吧?
我一开始就是这样想的,因此并没有觉得翻译一篇八千字的文章需要多久,这就是所谓初生牛犊不怕虎吧。
事后我觉得花费的心思至少是 1:10 以上,因为如果简单按照英文直译的话,语序不符合中文习惯,基本无法阅读。而且一句话有多种多样的译法,如何根据上下文选择恰当的译法也非常费神。
如何处理文中的习语?
比如文中有个 “Email Bankrupt”,一开始不知道什么意思,查字典后发现是指删除某个时间点之前处理不过来的邮件。最初翻译为「大概一年一次,她会宣布邮件破产,删除某个时间点之前处理不过来的邮件」,既有原文,也有意思,后面觉得太冗余了,最终定稿为「大概一年一次,她会删除某个时间点之前处理不过来的邮件」。
大概遇到了三四处类似地方,翻译的颇为纠结,只翻译意思的话失去了习语的韵味,就如同外国人译中国成语一样。直译为「邮件破产」,又需要读者了解习语,我想这样的读者应该会选择直接阅读原文。
信达雅有多难?
这次真切体会到了翻译之难,古人常说译文需信达雅,做到信已殊为不易,几经校对,最后上线的时候还是出现了错别字。为了译文畅通,更是百般修改,最后也不敢说读起来非常流畅。雅是不敢考虑了。
现在能理解为什么有些书籍翻译的那么之烂,因为这个事情需要投入大量心血,如果不是真正用心的人,很难做好这件事情。可想而知,古代翻译佛经的人,译文信达雅兼具,是多么呕心沥血。
翻译跟原文差距有多大?
我翻译的是产品相关文章,本身意思还是非常明了的,也没有晦涩的词汇。就算如此,也只能保证传递大概的意思,很多细微之处难以顾及。比如英文语序、习语之类的不可避免有着信息的损失。
我想,对于更文学化、个人化的书籍的翻译,比如小说、哲学、谈话录,翻译的损失就更不用说了。为了如实了解原文,自学对应的语言是不可避免的。
翻译有什么好处?
当时接下翻译,一方面是觉得文章不错,希望有更多人看到,另一方面也是觉得,在不断阅读输入的同时,也要尝试输出,不然很难保证自己真正理解了输入的东西。
从结果上来说,输出的效果非常显著,对文章整体内容的理解不是粗浅的读过一遍可比的。
说了那么久,到底翻译了什么?
文章内容是关于如何打造爆款产品的方法论和具体实践,来自国外一个两度创业成功的创业者。思路清晰,把爆款产品是什么,如何衡量并制定计划达成,进行了系统化的阐述并亲身实践,读完有醍醐灌顶之感,大呼过瘾。
有兴趣的可以点击阅读 我是如何一步步打造爆款产品,以及对应方法论 | 翻译
后记
大家有什么翻译的感想也可以聊聊,这次翻译之后感觉大伤元气,再也不敢随意夸口翻译很简单了。现在我非常佩服那些英文书的译者,为了传达知识,所花心血真的不少。
Shidenggui
作品
归档
订阅
我是如何一步步打造爆款产品,以及对应方法论 | 翻译
15 December 2019
缘起
之前在独立开发者社区的群里,有个小伙伴转发了一篇关于产品的文章,是关于一个两度创业成功的人,如何通过他的思考,总结出打造爆款产品的方法论以及实践过程,内容非常详细,可以说是手把手指导也不为过。
我读过之后大呼过瘾。当时小伙伴说他之前想翻译,但苦于没有时间,我就自告奋勇接下了这个任务。这里表示感谢群友嘉文的帮助和校对,使得翻译顺利完成。接下来就不多说了,请欣赏正文。
本文首发于个人公众号「食灯鬼」和「独立开发者」社区,若无授权,请勿转载。
公众号文章传送门
Superhuman 如何构建驱动引擎以达到产品市场契合
以下文章来自 Rahul Vohra,Superhuman 的创始人和 CEO,一家致力于最快邮件使用体验的初创公司。
所有人都听过产品市场契合(Product / Market fit,以下简称 pmf)驱动初创公司走向成功,而每一家失败的公司背后基本都潜伏着未达到 pmf 的问题。
对于创始人来说,从第一天起就有着尽快达到 pmf 的执念。它既是我们急于清除的巨大障碍,也是让我们夜不能寐、担忧永远无法达成的恐惧。但当我们想要弄明白到底什么是 pmf ,以及如何做到的时候,大部分人很快就认识到并没有一种经过实战检验的方式。
在 2017 年夏天,我正在极力为我的初创公司 Superhuman 找到一条达到 pmf 的道路。通过阅读一些经典的博客和影响深远的文章,我有了一些印象深刻的观察结果。YC 创始人 Paul Graham 认为 pmf 是你做了一些人们需要的东西,而 Sam Altman 认为 pmf 是用户开始自发的宣传你的产品。当然,最常见的定义来自于 Marc Andressed 2007 的 blog:
”判断 pmf 没有达到是很简单的。用户没有从你这里得到太多的价值,他们没有自发的宣传你的产品,使用量没有快速增长,新闻评论不温不火,销售周期过长,以及很多未完成的交易。
而判断 pmf 达到同样也很简单。用户以你所能生产的最大速度购买你的产品,使用量增长的跟你购买服务器一样快。用户把钱堆到你的公司账户。你尽你所能的招聘销售和客户支持人员。媒体人员不断的在打电话给你,因为他们知道你做了一些热门的好东西,想要采访你。你拿到了哈佛商业评论的年度企业家。投资者们蹲守在你的房前”
这是我对 pmf 的最生动的想象,这番画面让我泪流满面。
我们在 2015 成立工厂开始编写 Superhuman 的代码。一年后我们有了 7 个人,我们仍然还在狂热的编码中。到了 2017 年夏天,人数到达了 14 人,依旧是继续编码。这时,我感到了来自团队我和自己的,与日俱增的上线压力。我之前的公司 Rapportive 在更短的时间内启动、扩张并被领英(LinkedIn)收购了。然而此时,我们已经花了两年时间,却什么还没有。
但是不管压力多么巨大,我还是没准备好上线。通常大家都说“不管怎么先上线,看看反响再说”,这对那些只投入了几个月时间,沉没成本还不高的公司可能适用。但是”先上线看看反响“对我来说,太不负责任和鲁莽了,特别是想到我们投入的这几年时间。
而我身为创始人不能把我的感受直接告诉我的团队,这也加剧了我的压力。这些雄心勃勃的工程师为这个产品倾注了全部心血。我没办法跟他们说这个产品还没准备好,更糟糕的是,也没想到任何办法摆脱这种情况。我一直在努力,想要找到正确的语言或者框架来搞清我们当前所处的位置,并规划好之后到达 pmf 的步骤。
因为我找到的关于 pmf 的描述对上线后的公司非常有帮助。如果上线后,收入没有增长,融资困难,媒体毫无兴趣,用户增长非常贫乏,你可以说你没有达到 pmf。但实践中,因为我之前创业成功,在融资上并没有遇到问题。我们有意拒绝了媒体采访邀约。当前我们选择只服务一小部分内测用户,所以也谈不上快速增长。对于处在内测阶段的我们来说,此时没有任何指标可以明确是否达到了 pmf。
这些关于 pmf 的描述都太事后诸葛亮了,太过不切实际。我清楚的知道公司的情况,却没办法把这一点传达给其他人,也不知道公司下一步的计划。
所以我绞尽脑汁想要搞清楚如何让 Superhuman 从当前的位置更进一步,达到 pmf。最后我开始思考:如果我们能衡量 pmf 呢?因为如果你能衡量 pmf,或许你就可以改善它。甚至你可以系统的提高 pmf 直到达到你的预期。
围绕这一目标重新定位,我开始反推达到 pmf 的过程。下面是我的发现,特别是如何清晰的定义 pmf 指标并构建 Superhuman 驱动引擎达到 pmf 的四个步骤。
围绕一个指标:达到 pmf 的领先指标。
在我理解 pmf 的过程中,我读了所有我能读到的书,并与每一位我能找到的专家交流。当我遇到肖恩·埃利斯(Sean Ellis)时,一切都改变了。他在 Dropbox、LogMeln 和 Eventbrite 创立之初负责早期增长,后来又创造了“增长黑客”(growth hacker)一词。
我找到的 pmf 定义非常形象和具有说服力,但它们是滞后指标——当投资人蹲守在你的房子外时,你已经达到了 pmf。相反,埃利斯找到了一个先行指标:只需要问问用户,如果你无法使用我们的产品,你会感到多么失望?然后记录回答“非常失望”的人的比例。
埃利斯通过调查将近 100 个初创公司,发现了魔法数 40%。通常少于 40% 的用户回答“非常失望”的公司都努力寻求增长,而那些具有强大吸引力的公司几乎总是超过这个门槛。
问问你的用户,如果他们不再使用你的的产品的话,他们有什么感觉。那些回答“非常失望”的人是解锁 pmf 的钥匙。
Hiten Shah 做了一个有用的尝试,他在 2015 的开放研究项目中,发了埃利斯的问题给 731 个 Slack 的用户,51% 的用户回答如果没有 Slack 的话,他们会非常失望。由此可见 Slack 在 50 万付费用户时已经达到了 pmf。如今,考虑到 Slack 传奇般的成功故事,这点显而易见。不过由此可以看出达到 40% 这个指标有多么艰难。
在这种方法的启发下,我们开始测量 Superhuman 的反响怎么样。根据埃利斯的建议,我们以最近两周使用过产品核心功能至少两次的用户为准。(当时我们应该有 100 到 200 个可调查用户,但是规模更小、更早期的初创公司也不要羞于使用这个方法 —— 大概 40 个左右的回复就可以给出大概正确的方向,远比大部分人想的少)
然后我们开始通过邮件给这些用户发送调查问卷询问以下问题:
如果你不再能使用我们的产品,你的感觉如何?
A. 非常失望
B. 有点失望
C. 不失望
你认为我们的产品对哪些人最有用?
你觉得我们产品最有用的地方是哪里?
怎样才能让我们的产品变得对你来说更有用?
通过收集的反馈,我们开始分析第一个问题:
只有 22% 的人投票给“非常失望”这个答案,这清楚的表明 Superhuman 没有达到 pmf。虽然这个结果令人沮丧,但我却充满了斗志。我现在有了一个工具可以向团队解释当前的处境,更令人兴奋的是,我们据此有了一个可以提高 pmf 的计划。
从基准到驱动引擎:优化 pmf 的四个步骤
下定决心后,我把所有精力都聚焦在如何提高我们的 pmf 。我们需要形成一个达成 pmf 的框架,而用户对每个调查问题的反馈都是该框架的关键素材。
以下是我们 pmf 引擎的四个组件:
但其中的许多用户可能不是你的目标用户,他们并不真正需要你的产品,你的产品也并不适合他们。你并不想这些人成为你的用户。
作为早期团队,你可以先入为主的将目标市场限制在你认为的细分用户上,但这并不能教给你任何新东西。相反,如果你使用回答“非常失望”的用户作为一个透镜去缩小市场,数据本身就能说明问题,甚至你可能找到新的非常契合你的产品的市场。
对我来说,细分的目的是为了找到产品能拥有更好的 pmf 的领域,这些领域我可能忽略了,或者没有考虑到。
一开始,我们将用户按照第一个问题分组(如果你不再能使用我们的产品,你的感觉如何?)
我们给每个填写问卷的用户分配了一个角色。
接下来,我们研究「非常失望」组的用户(占比最大的 22% 支持者),并利用这些特征来细分市场。在这个简化的例子中,你可以看到我们主要关注创始人、经理、高管和业务发展人员——暂时忽略了所有其他角色。
通过这种更加细分的视角来观察我们的数据,数字变了。通过聚焦「非常失望」组的用户,我们的 pmf 上升了 10 % 之多。虽然离渴求的 40% 还有一点距离,但是我们用最小的努力拉近了距离。
为了更加深入的探索,我想更好的了解真正热爱我们产品的用户。我希望尽可能生动的描绘他们的用户画像,以便激励我的团队给他们更好的服务。
我利用 Julie Supan 的高期望客户框架来做到这点。Supan 指出,高期望客户(HXC)并不是一个具有多重角色的用户,而是在你目标群体中最有洞察力的用户。最重要的是,他们能从你的产品中获得最多好处,并帮你传播口碑。例如, Airbnb 的高期望客户并不只是想要访问新地方,同时也希望拥有归属感。对 Dropbox 而言,他们的高期望客户希望保持井然有序,简化生活并保证他们的工作成果不受损害。
带着这种想法,我试图找出 Superhuman 的高期望客户。我们基于「非常失望」组的用户分析他们对第二个问题的反馈:你认为我们的产品对哪些人最有用?
这是一个非常有力的问题,因为快乐的用户通常都会描绘他们自己,而不是其他人,使用那些对他们最关键的词语。你会了解你的产品对哪些人有用,以及那些会与他们产生共鸣的语言(也为你的市场营销提供了非常有价值的核心视野)。
基于用户的语言 和 Supan 的建议,一个栩栩如生的 Superhuman 高期望客户的用户画像出现在我们眼前:
“妮可是一个努力工作的专业人士,她与许多人打交道。举例来说,她可能是 CEO、创始人、经理或者业务发展人员。妮可长时间工作,往往周末也不休息。她认为自己非常忙,希望能有更多时间。虽然妮可自认很有效率,但她意识到自己还可以做的更好,偶尔会花时间想办法提高自己。她大部分的时间都花在她的收件箱,每天大概阅读 100-200 封邮件以及发送 15-40 封邮件(忙的时候一天可能发送多达 80 封)。 ”
“妮可认为及时响应是她工作的一部分,她也自豪自己能做到这一点。她知道反应迟钝会阻碍她的团队,损害她的声誉或错过机会。她的目标是清空收件箱,但一周往往只能做到两三次。偶尔——大概一年一次——她会删除某个时间点之前处理不过来的邮件。她通常具有成长型思维,对新产品和新技术保持热情,但是在邮件处理这块,她可能还是旧的思维方式。每当使用新的邮件客户端时,她都会怀疑这是否能提高她的效率。”
基于高期望客户,我们整个公司可以比其他任何人都更聚焦于这个细分市场。有些人会争论说这个方法太局限了,创业早期不应该把目光弄的如此狭窄。
”很多人认为将产品局限于一个小的细分市场,会很容易到达天花板——但我不这么认为。”
Paul Graham 解释了为什么:
“当开始创业时,一定有某些用户需要他们的产品——这不是指那些未来可能用到这些产品的人,而是那些现在就迫切需要的人。通常情况下,初期用户群很小。因为如果存在大量用户迫切需要的东西,而且可以通过一个创业公司来满足的话,通常这样的产品已经存在了。这意味着你只能两者选一:要么给大部分人提供有点想要的东西,要么给小部分人提供非常想要的东西。选择后者,不是所有这样的创意都是好的创业点子,但几乎所有好的创业点子都是这类的。”
在另一篇文章中,他对这个观点进行了更加深入的探讨:
“在理论上,这种类型的创业会给初创公司带来麻烦。它们可能会达到一个局部最优值。但实际上,这种情况从未发生。创意的极大值很少是孤立的,大部分很棒的想法都挨着更好的想法。“
从本质上讲,最好做给少部分用户提供「非常想要」的产品,而不是给大量用户提供「有点想要」的产品。在我看来, 对一款为少量用户提供「非常想要」的功能的产品,精确定位细分市场很有帮助,可以减少很多不必要的工作。
2)通过分析用户反馈,将中立用户转化为狂热粉丝
然而,仅仅辨识出高期待客户是不够的。我们已经细化了目标用户,现在需要更加深入的挖掘他们的需求。因为我们目前的指标低于 40%,所以需要找出核心用户喜欢 Superhuman 的真正原因——以及如何将更多用户转化成核心用户。
为了深入了解我们将如何改进产品并扩大其吸引力,我发现将精力集中在以下关键问题上很有帮助:
为什么用户喜欢这个产品?
是什么阻碍了人们对产品的喜爱?
为了理解用户为什么喜欢 Superhuman,我们再一次将目光转向那些离开了我们产品会非常失望的人。这一次,我们关注他们对第三个问题的回答:你觉得我们产品最有用的地方是哪里?
下面是一些引人注目的答案:
“使用 Superhuman 可以更快地处理电子邮件。它一次只显示一封邮件,总体速度比Gmail 快得多。我用一半的时间就能清空收件箱。”
“速度!这个应用程序速度快的惊人。搭配良好的用户体验和快捷键,我感觉自己像个真正的超人。”
“使用 Superhuman 比使用 Gmail 快得多,不是一点半点。而且它复用了 Gmail 的快捷方式,对用过 Gmail 的人来说上手毫不费力。”
“我可以更快地处理收到的邮件,对消息进行快速排序并简化我的工作流程。”
“速度。审美。我能通过键盘搞定所有事。”
“速度和出色的键盘快捷键。我几乎不需要触控板。”
通过把这些答案输入到词云,出现了一些共同的主题:热爱我们产品的用户最欣赏它的速度、专注以及键盘快捷键。
通过对产品吸引力的深入理解,我们把注意力转向了如何让更多的用户爱上 Superhuman。
我们接下来要做的可能有点反直觉:我们决定礼貌的忽略来自那些离开这个产品不会感到失望的用户的反馈。
这些用户不应该对你的产品战略有任何的影响。他们会提一些毫无意义的需求,提供病态的用户案例,同时可能非常活跃,再之后他们会毫不犹豫的抛弃产品,留下你一塌糊涂的产品规划。尽管这看起来令人惊讶或痛苦,但不要对他们的反馈作出行动——它将使你在追求 pmf 时误入歧途。
礼貌的无视那些离开你的产品不会感到失望的用户. 他们离爱上你的产品太远了,让他们改变基本上是不可能做到的。
这样就只剩下那些离开我们的产品会感到有点失望的用户了。在某一方面,“有点”意味着机会。吸引的种子已经播下,也许通过一些调整,就可以让他们爱上你的产品。但另一方面,很可能无论你做什么,某些用户都不会爱上你的产品。
为了充分利用这些提供线索的用户,我们进行了进一步的细分。通过分析第三个问题,我们发现喜欢产品的用户认为最有用的地方是速度很快。我们利用这个特征进一步划分用户组。
我们将用户通过速度是否是他们的主要关注点划分为了两组,并通过他们的反馈采取行动:
对那些有点失望,但速度不是他们主要关注点的用户组:我们选择礼貌的无视他们,因为我们最主要的产品功能没有跟他们产生共鸣。就算我们开发了所有他们想要的一切,他们也不太可能爱上这个产品。
对那些有点失望,但速度是他们主要关注点的用户组:我们重点关注他们的反馈,因为他跟我们的主要功能产生了共鸣。某些功能——可能是一些小功能——就能让他们爱上我们。
聚焦在后一个用户组,我们开始更仔细的研究他们对第四个问题的回答:“怎样才能让我们的产品变得对你来说更有用?”
这是我们看到的:
通过一些分析,我们发现阻碍用户爱上产品的原因非常简单:缺少移动端应用。在 2015 年,我们采取了先从桌面端开始的策略。因为大部分邮件是从桌面端发送的,我们觉得在这里可以创造最大的价值。我们一直计划构建一个移动端应用,但是在我们创业早期——就像每个初创公司一样——我们的筹码只够下注一次。在 2017 年,移动端应用的紧迫性已经非常明显了,它成为了我们 pmf 的关键。
通过更深入的研究,我们发现了一些不太明显但是更有趣的需求:集成第三方、附件处理、日历、统一收件箱、更好的搜索、阅读回执以及其他一些长尾需求。例如,作为一个初创公司,我们内部并没有重度使用自研的日历,所以并没有像重视邮件一样重视日历。通过这次对用户反馈的深入挖掘,我们提高了日历在我们产品中的优先级。
对我们产品的主要优势和缺点有了清晰的认识后,接下来需要做的就是把这些认知应用到构建产品当中。通过满足那些中立用户的需求,尽量让他们转变为狂热用户。
最终我认识到:如果你仅仅专注于增强用户喜欢的功能,你的 pmf 并不会提高。如果你仅仅专注于解决妨碍用户爱上你的问题,你的竞争对手很可能超过你。这个结论指导着我们的产品规划,帮助高效的制定了路线图。
为了让「非常失望」组的用户更喜欢我们,路线图的一半用于以下项目:
更快的速度:我们的产品已经很快了,但我们努力让它更快。例如,当前 UI 的响应时间在 100 ms 内,搜索速度比 Gmail 更快。但我们进一步将响应时间缩短到 50 ms 内,并即时呈现搜索结果。
更多的快捷键:用户喜欢通过快捷键搞定一切事情。因此我们把快捷键变得更健壮和全面。我们加了一些其他邮件客户端从没有过的快捷键,同时保证当用户输入超过电脑处理速度时,依旧能得到正确的处理结果。
更多自动化:用户真的非常在乎效率。但打字速度限制了效率的提高。因此我们开发了代码片段,允许用户自动输入短语、段落、甚至整个邮件。为了节省更多时间,我们进一步完善了代码片段,使其更加健壮,可以添加附件、自动添加抄送人,甚至可以和 CRM 与 ATS 集成。
更好的设计:在我们收集的反馈中,看到用户喜欢我们的设计以及一些小细节,因此我们投入了更多精力,做了很多细微的改动,以向用户表达我们对细节的关注。例如,输入“–>" 将自动转成一个右箭头:→。
为了争取那些「喜欢我们产品的速度但是有点失望」的用户,我们的第二部分路线图主要关注如下:
开发移动版 App。
增加与现有产品的集成能力。
改进附件处理。
增加日历功能。
创建一个统一收件箱选项。
改进搜索。
增加已读回执功能。
为了给这些工作排序,我们进行了非常简单的成本收益分析:我们给每一个潜在的功能点打上低/中/高的成本标签,以及类似的低/中/高的收益标签。对于路线图中第二部分用于争取「有点失望」的用户的功能,收益很容易计算,只需要统计问卷中对该功能的改进建议数量。对于第一部分,让「非常失望」的用户更喜欢我们的产品,我们只能通过直觉判断收益水平。这是「产品本能」发挥作用的地方,而这种能力基于经验和与客户共情的能力(早期刻画高期待客户的用户画像,对训练产品本能非常有帮助)。
有了上述计划,我们就从「低成本、高收益」的功能点开始尽快发布改进。
为了增加你的 pmf 分数,花一半的时间继续投入用户已经喜欢的功能,另一半时间解决阻碍用户喜欢上你的产品的问题。
回答「非常失望」的用户百分比迅速成为我们最关心的数字。作为最明显的指标,我们分别以每周、每月、每季度的频率跟踪它。为了让这个过程简单点,我们开发了一些定制化工具持续对新用户进行问卷调查,并以不同的时间窗口聚合结果。我们也调整了产品团队的目标,以「非常失望」的用户百分比作为唯一的关键结果设置 OKR,以确保我们持续增加 pmf 分数。
重新调整了产品方向后很快有了成效。在 2017 年夏天刚开始的时候,我们的 pmf 分数为 22%。通过细分仅关注「非常失望」的用户组后,分数为 33%。通过三个季度的努力,分数几乎翻倍达到了 58%。
事情并没有到此结束,我们将一直保持对 pmf 分数的追踪。我认为对于初创公司来说关注该指标是非常有用的,因为随着用户增长,你将会遇到各种各样的用户。早期用户是非常宽容的,尽管产品有着这样那样的问题,他们依旧能享受它带来的好处。但随着你的用户增长,他们的要求会越来越高,渴求其他竞品已有的功能。此时,你的 pmf 分数可能会大大下降。
然而,这没什么好焦虑的,因为总有办法解决。如果你的产品有很强的网络效应(想想 Uber 或者 Airbnb),那么产品价值将随着用户增长同步增长。如果你是一个像 Superhuman 一样的 SaaS 公司,随着用户的增长,你只需要持续改进产品。为了做到这一点,我们每个季度都会用本文提出的这套方法重定我们的路线图,确保足够快的改进 pmf 分数。
即将迫降,小心冲击
随着这个过程的不断演进,我发现了如何定义 pmf 以及衡量它的办法。我们团队通过一个具体指标凝聚在一起,而不是一个令人绝望的抽象目标。通过调查用户,细分市场,了解用户喜欢什么和讨厌什么,然后制定路线图平衡这两点,我们找到了提升 pmf 的方法论。
很难高估 pmf 驱动引擎在我们公司的影响。我们在 Superhuman 所做的一切——从招聘销售、营销人员到融资——都变得轻而易举。团队扩张到了 22 人,NPS(净推荐值)也随着 pmf 不断增长。无论是调查问卷中,还是社交媒体上,用户对产品的喜爱明显提高了。当前的投资者开始问我能不能在下一轮融资前追加更多资金,而外部投资者不断问我他们是否能够投资。
再一次回顾从为 Superhuman 构建 pmf 引擎中所学到的东西,我得到了两个结论:
早期投资者应该建议初创团队在达到 pmf 之前避免推动增长。在这个行业中,我们都知道这最终会导致灾难,但过早增长的压力仍然很普遍。初创公司需要时间和空间来找到适合的方式并正确启动。
对于任何想要走出荒野,踏上追寻难以捉摸的 pmf 之路的创始人来说,我已经体验过这一切 —— 我希望你能在那些众所周知的创业车库中重组我的引擎,让它属于你。当你最终达到你所追求的 pmf 分数时,我建议你一脚踩下踏板,尽可能的高速增长。尽管这会令你感到不适,但你会获得你需要的证据证明你将成功。
后记
这是我第一次翻译英文文章,全文译好八千余字,花了不少心思,也算真切体会到了翻译之难,古人常说译文需信达雅,做到信已殊为不易。为了防止误译,经过几番校对才敢发布。但因为经验原因,肯定还有不足的地方,希望大家能指出,以便改进。
当时接下翻译,一方面是为了觉得这篇文章非常不错,希望更多人看到,另一方面也是觉得自己译过一遍,能加深对内容的理解,而不是草草读过。希望大家也能有所收获。
原文地址
Shidenggui
作品
归档
订阅
设计杂谈(一):什么是美?
7 December 2019
缘起
上周读完了吕澄先生的《印度佛学源流略讲》、《中国佛学源流略讲》,言简意赅,真的是字字珠玑。
好奇之下就搜索了先生的其他作品,偶然间看到了一本小书,名为《美学浅说》。
dxneim
最近一直在看一些设计方面的书以改善我的网站设计,所以觉得这本书应该对我的目标有所帮助。
民国影印本
到手之后发现,这本书初版于民国,新出的版本为了忠实还原采用的是影印方式,内容为繁体竖排,同时使用的是民国时期的白话文,跟现在通行的略微有差别。
rtvdjO
虽然有这些小问题,但这本书是入门书,本身就是科普性质,所以读起来并不费力。总共就 45 页,今天花了两三个小时读完了,感觉还是有所收获的,详见正文。
何者是美?
美是生命共有的普遍倾向得到舒展而产生的一种喜悦。
什么是生命共有的普遍倾向?
这倾向既有生理的,也有心理的。生理上来自于我们远古祖先在基因中的痕迹。
例如颜色,红是血的颜色,给人强烈的感受,绿色是植物的颜色,给人以宁静、闲适之感。
而心理上最重要的就是移情作用。
何谓移情?
见大海波涛汹涌而心潮澎湃之感。大海本自无情,何来澎湃?实是内心攀援外境而生,却假认是大海的情绪,这就是移情。
HEFvLg
读悲剧流泪也是同理,文字本身并无悲喜之分,它只是映射我们内心的情感,我们却反道文字有悲有喜。
KBGnwz
什么是艺术?
艺术是美的显现和具象化,可激起他人美的感受。
什么是艺术家?
人感受美的程度不同,表现美的能力也有所不同。能将自己感受的美表现出来的就是艺术家。
所以人人可做艺术家,正所谓吃饭喝水皆是修行,若非如此,美的社会只是空中楼阁。
何谓美的作品?
“因为生命自然的表白,固属喜欢就笑,苦痛就哭。但尤其自然的,是顺着生命普遍的性质去悲哭喜笑,便使凡有生命的谁都不能不这样喜欢的笑,又谁都不能不这样苦痛的哭。”
鲁迅先生曾说“人类的悲欢并不相通,我只觉得他们吵闹”,而美正可做为一剂解药。
尾声
在我心中,美不再只是一个抽象的概念,而具有了更鲜活的形象。
正如吕澄先生所言 “艺术和人生只有一种关系,便是实现「美的人生」”
Shidenggui
作品
归档
订阅
读《Elon Musk》:传奇人生之旅的开端(一)
2 December 2019
缘起
之前有个埃及出差的大学同学回国培训,路经上海,一起聚餐的时候逛了逛现代书店,偶然看到了这本书。
LN18Ry
马斯克可以说是目前风头正盛的企业家。作为男性,小时候基本都有对于太空的浪漫幻想,但是在成年后能立志于此并有所作为的人很少。而马斯克最传奇的应该就是在太空行业,以一人敌一国的成就了。
这就引起了我的好奇心,马斯克是怎么做到的呢?从一个程序员跨界汽车和航天,而且在这些行业,都做出了非常创新的成果。
安兰德把人分为两类,奋进者和掠夺者。他们之间的区别在于掠夺者致力于争夺已有的蛋糕,玩的是一个零和游戏,因此种种权谋机巧,勾心斗角,毕竟你亏就是我赚,中国几千年的历史很多时候就是这样一个回环。而奋斗者不然,致力于把蛋糕做大,虽然创新如同拓荒,充满危险和挫折,但是成果却由全体共享,这个过程本身就充满了积极向上的进取精神,自然振奋人心。马斯克可以说是奋进者的典型人物。
pifzf9
因此买下这本书也是理所当然的事情了。一本标价 60 的口袋书,能满足我的好奇心,想想还是很值的。
口袋书的优势就是可以揣在兜里,有事没事就可以掏出来看看,最常见的就是在地铁或者排队时,书大概 400 多页,虽然是纯英文的,但是最近购入了有道词典笔,使得阅读效率提高了不少,就这样断断续续花了一个月时间读完了这本书。
dSQQLP
家庭
写传记的人都喜欢上溯祖辈,大抵环境对一个人的影响是极大的,脱离背景去谈一个人难免虚浮。
而马斯克一家人骨子里就充满了冒险和叛逆精神精神。
他爷爷 Haldeman 是加拿大人,出生于1907年,幼时丧父,成年后靠自己打拼有了不错的家产,同时购置了私人小飞机。他时常带着老婆孩子开飞机巡游北美大陆。当时的小飞机质量可想而知,带着全家人一起冒险自然不受周围人的认同,就算如此老爷子也不改初心。
二战后不久的 1950 年,因为无法忍受加拿大政府对个人自由的干涉愈演愈烈,遂抛弃在加拿大的一切移居南非。为了带走心爱的飞机,他们将它拆成零件以便带到南非重新组装起来。而几十年后,马斯克也继承他爷爷的自由天性,从南非长大,到加拿大求学,最后到达美国追求梦想。
老爷子在南非继续延续他的冒险作风,不是开着飞机带着老婆环绕非洲大陆兜风,就是驾着越野车在荒原上奔驰野营,其中遇到的危险自然也不少。曾经有次因为汽车出故障在沙漠中露宿三天,他们出去找食物度日直到修好汽车。最后老爷子还出了本书描述他的冒险生活。
这样性格的老爷子,他对子女的教育自然采取的是放任自由的方式,据书中所说,他从没去过孩子的学校以及出席过任何家长会,就算他儿子曾经是篮球队队长和优等生,也时常因为出去冒险而将小孩独自扔到家中。
“父亲认为我们有能力处理任何事,我们只需要做出决定然后行动搞定它”。
1948年马斯克的妈妈 Maye Elon 就是在如此情况下出生。在这样的氛围下,她的独立自主性、冒险精神和对风险的容忍度自然远超一般人。
少年
马斯克的妈妈小时候就长的非常出众,高中开始兼职模特。而马斯克的老爸跟她的青梅竹马,虽然他妈妈一开始并不喜欢他爸,但是耐不住死缠烂打,最后还是同意求婚走到了一起。
他们算是一个典型的中产阶级家庭,他父亲是一名工程师,生活还算富裕。
1971 年马斯克出生,一年后他的弟弟 Kambal 出生。
马斯克自小就是以一个书呆子的形象长大,时常发呆沉浸在自己的世界中,对外界不闻不问,他父母一度以为他有疾病而选择手术摘除了他的咽扁桃体,但情况并没有改善。
Q7L6HN
小时候的马斯克对书籍的热爱远超常人,据他回忆,学校下午两点放学,他通常会跑到书店呆到下午六点为止。就在这段时期他阅读了基地三部曲、银河系漫游指南、指环王等小说,并将这些作品列入他的最爱作品之中,这也是马斯克后期对各种经典科幻作品致敬的思想来源。我想为什么马斯克能得到那么多的喜爱,一部分也是来自于他对这些经典作品的熟悉,给他赋予了更多极客气质。
iMUru8
同时经典科幻作品通常格局宏大,对人类的未来无比乐观,肯定人的主观能动性,充满了浪漫的冒险主义精神和勇敢的探索精神。
我还记得我小学看的凡尔纳的《从地球到月球》,一群科学家及冒险家合力共建炮弹,把两人一狗送到月球,因为意外,炮弹环绕月球却无法登陆,后面主角们利用各种方法重新回到地球。
FmgtTZ
在这些作品里,只要符合科学原理,没有不可能的事情,只要配之以冒险精神,没有做不到的事情。这也是马斯克常常异想天开的原因吧。
“在某个时间点,我读遍了学校图书馆和邻居家所有我喜欢的书。大概我三四年级的时候,说服了本地的图书馆借阅我预定的书籍”
不过因为书呆子的性格,马斯克在学校收到不少霸凌,有次被人无缘无故暴揍了一顿导致鼻子骨折导致需要整容鼻子。
雪上加霜的是他父亲婚后脾气不好,因此父母生活并不幸福,不久就离了婚,而马斯克和他弟弟跟了他妈妈。
比较神奇的是十二岁左右马斯克跟他弟弟又回去跟父亲生活。据他妈的说法是因为他老爸一直私下跟马斯克他们联系,说他太寂寞,希望孩子过去陪他们。
而马斯克跟他父亲生活的这段时期过的非常不愉快,虽然书里没有明说,但是他父亲应该是有点虐待倾向。
十岁时马斯克接触到了电脑上,马上为之着迷并预定了一台,这可是在 1980 年代初。还有值得一提的是在十二岁时他开发了一款打飞机游戏,登上了当地杂志,虽然游戏很简陋,但也是一个了不起的成绩。
所以马斯克的童年虽然生活较为富裕,但是却并不愉快,这也导致他更加沉浸于自己的世界之中。而这样的性格,同南非的种族歧视以及动荡的政治氛围格格不入,自然也种下了马斯克离开南非到加拿大,甚至美国的种子。
尾声
虽然书已经看完了,但是梳理内容要花不少时间,这里先奉上第一部分,后续还有加拿大的求学生涯以及在美国的追梦之旅。
好久没发文章了,之前忙于开发自己的小说推荐网站「推书君」(对小说有兴趣的可点击阅读原文访问)。现在网站已经告一段落,之后会慢慢恢复文章的更新,再次对还关注我的读者表示感谢,多谢你们的支持。
有任何想法、建议欢迎留言。
Shidenggui
作品
归档
订阅
推书君网站上线了
28 July 2019
一直挺喜欢看网文的,而喜欢看网络小说的人免不了受到书荒的困扰。到了现在,感觉还是没有非常好的解决书荒的方法。
所以最近一段时间跟朋友一起写了一个推荐网络小说的网站,叫推书君,地址为 www.tuishujun.com 。后续希望能借助相关的算法实现更好的网文个性化推荐。
首页
LZu6Qi
相似小说
重写了搜索相似书籍,优化了相关的算法和更新速度,下面是我正在看的几本小说的搜索结果:
A15arr
eF6K2B
sGglcp
自定义标签
支持类似豆瓣的自定义标签功能,用户可以为书籍打标签,同时也可以通过标签来检索对应的书籍:
lKZPgJ
TQUphl
试读以及评分
2lps80
除此之外还包含了书单、书架功能。不过因为网站才刚刚开始,所以数据什么的不是很全面,正在努力完善中。
如果对网络小说、后续开发计划有兴趣的话,欢迎加 qq 群 833400329 或者我的微信一起交流。
Shidenggui
作品
归档
订阅
LRUCache 的实现
22 April 2019
缘起
刷 leetcode 的时候碰到的这道题。LRUCache 在现实中也经常用到:
内存换页,需要淘汰掉不常用的 page。
缓存函数的结果,比如 Python 就自带的 lru_cache 的实现。
redis 在设置了 maxmemory 时,在内存占用达到最大值时会通过 LRU 淘汰掉对应的 key。
要求
Leetcode 题目要求如下
/*
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and put.
get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value) - Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
Follow up:
Could you do both operations in O(1) time complexity?
Example:
*/
LRUCache cache = new LRUCache( 2 /* capacity */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // returns 1
cache.put(3, 3); // evicts key 2
cache.get(2); // returns -1 (not found)
cache.put(4, 4); // evicts key 1
cache.get(1); // returns -1 (not found)
cache.get(3); // returns 3
cache.get(4); // returns 4
关键点在于 get 和 put 操作的时间复杂度都需为 O(1)。
初版
为了实现 O(1) 的复杂度,使用哈希表来存储对应的元素。然后通过双向链表来实现 lru cache 相关的逻辑,
在 get 时,将命中的节点移动到头部
在 put 时如果命中已存在的节点,参照 get 操作,如果为新节点
达到最大大小,删除尾部节点,然后在头部插入新节点
未达到最大大小,则直接在头部插入新节点即可
class Node:
def init(self, key, value, prev=None, next=None):
self.key = key
self.value = value
self.prev = prev
self.next = next
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self._map = {}
# linked list with head
self._list = Node(None, None)
# 使用 last 指针来加快删除最后一个节点的速度
# 在以下情况需要更新 last 指针
# 1. 第一次插入数据时,需要将 last 指针指向新插入的节点
# 2. 删除最后一个节点时,需要将 last 指针前移
# 3. 当 get 或者 put 命中节点需要移动到链表顶端时。
# 如果需要移动的节点正好是最后一个节点(这时候 last 指针才有可能发生变化)
# 而且 last 指针之前不是头结点(如果是的话,移动后该节点还是尾节点,不需要变动 last 指针)
self._last = self._list
self.size = 0
def get(self, key: int) -> int:
if key not in self._map:
return -1
node = self._map[key]
self._move_to_first(node)
return node.value
def _move_to_first(self, node):
# 当移动的节点为最后一个节点,且该节点之前不为头结点
# 将 last 指针前移
if node == self._last and node.prev != self._list:
self._last = node.prev
# 首先删除当前节点,要额外考虑该节点为最后一个节点,
# 此时不需要调整之后节点的 prev 指针
node.prev.next = node.next
if node.next is not None:
node.next.prev = node.prev
self._insert_to_first(node)
def _insert_to_first(self, node):
node.next = self._list.next
node.prev = self._list
self._list.next = node
# 插入到头结点之后,如果该节点不是最后一个节点
# 同样要调整之后节点的 prev 指针
if node.next:
node.next.prev = node
def put(self, key: int, value: int) -> None:
if key in self._map:
node = self._map[key]
node.value = value
self._move_to_first(node)
return
self.size += 1
node = Node(key, value)
self._map[key] = node
if self.size > self.capacity:
# 直接根据 last 指针删除最后一个节点,然后前移 last 指针
self._last.prev.next = None
del self._map[self._last.key]
self._last = self._last.prev
# 如果插入时为空链表,设置 last 指针
if self._list.next is None:
self._last = node
self._insert_to_first(node)
Leetcode 经典实现
初版实现的方法,需要不断判断删除,移动的是不是尾部指针,引入了很多不必要的 if 判断。而 Leetcode 讨论区里面提出了一个更好的方法。
原本我们通过引入 Dummy Head 已经简化了头部相关的操作,这里额外再引入一个 Dummy tail ,这样的话在移动删除尾部节点的时候就不需要额外判断了。
class Node:
def init(self, key, value, prev=None, next=None):
self.key = key
self.value = value
self.prev = prev
self.next = next
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self._map = {}
# linked list with dummy head and tail
self._list = Node(None, None)
self._last = Node(None, None)
self._list.next = self._last
self.size = 0
def _delete_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def get(self, key: int) -> int:
if key not in self._map:
return -1
node = self._map[key]
self._move_to_first(node)
return node.value
def _move_to_first(self, node):
self._delete_node(node)
self._insert_to_first(node)
def _insert_to_first(self, node):
node.next = self._list.next
node.prev = self._list
node.next.prev = node
self._list.next = node
def put(self, key: int, value: int) -> None:
if key in self._map:
node = self._map[key]
node.value = value
self._move_to_first(node)
return
self.size += 1
node = Node(key, value)
self._map[key] = node
if self.size > self.capacity:
del self._map[self._last.prev.key]
self._delete_node(self._last.prev)
self._insert_to_first(node)
Python 内置的 LRUCache 实现
但是上面的方式还有一个问题,就是在 lru_cache 满了的时候,此时新增一个节点,会导致需要从链表中删除一个尾部的旧节点,然后同时在头部插入一个新节点。
有没有办法直接使用旧的删除的节点来代替新增的节点呢?这样在 LRUCache 满了的时候,put 新元素的性能会获得很大的提升。
而 Python 内部实现正是考虑了这一点,利用了带头结点root的循环双向链表,避免了该问题。
添加新元素时,如果容量达到最大值,则直接利用 root 作为新节点,使用原来的尾部节点即 root.prev 作为新的 root 节点。
使用 list 代替 node 节省空间。
下面是 Python 的 lru_cache 的实现,因为原实现是装饰器,这里略作修改为类的实现:
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self._cache = {}
# circular queue with root
self.root = []
self.root[:] = [self.root, self.root, None, None]
self.size = 0
def get(self, key: int) -> int:
if key not in self._cache:
return -1
node = self._cache[key]
self._move_to_front(node)
return node[RESULT]
def _delete_node(self, node):
node[PREV][NEXT] = node[NEXT]
node[NEXT][PREV] = node[PREV]
def _insert_to_front(self, node):
node[NEXT] = self.root
node[PREV] = self.root[PREV]
node[PREV][NEXT] = node[NEXT][PREV] = node
def _move_to_front(self, node):
self._delete_node(node)
self._insert_to_front(node)
def put(self, key: int, value: int) -> None:
if key in self._cache:
node = self._cache[key]
node[RESULT] = value
self._move_to_front(node)
return
self.size += 1
if self.size > self.capacity:
# 直接使用 root 节点作为新节点,然后通过将 root[NEXT] 的待删除节点设为新的 root 节点,避免了删除和分配新节点的消耗
self.root[KEY] = key
self.root[RESULT] = value
self._cache[key] = self.root
self.root = self.root[NEXT]
del self._cache[self.root[KEY]]
return
node = [None, None, key, value]
self._cache[key] = node
self._insert_to_front(node)
redis 的 LRU 淘汰算法的实现
因为想到 redis 也实现了 lru cache,就抽空看了下源码,发现跟想象中非常不一样,并不是常规的实现方式。
当 redis 达到设置的 maxmemory,会从所有key 中随机抽样 5 个值,然后计算它们的 idle time,插入一个长度为 16 的待淘汰数组中,数组中的 entry 根据 idle time 升序排列,最右侧的就是接下来第一个被淘汰的。淘汰后如果内存还是不满足需要,则继续随机抽取 key 并循环以上过程。
因为是随机抽样,所以分为以下情况
抽样的 key idle 小于最左侧最小的 idle time,什么都不做,直接跳过
找到适合的插入位置 i
如果 pool 还有剩 余空间,右移 pool[i: end]
没有的话,左移pool[0: i + 1],这样的话 idle 时间最小的就被淘汰了
关键实现逻辑如下:
while (mem_freed < mem_tofree) {
sds bestkey = NULL;
struct evictionPoolEntry *pool = EvictionPoolLRU;
while(bestkey == NULL) {
evictionPoolPopulate(i, dict, db->dict, pool);
/* Go backward from best to worst element to evict. */
for (k = EVPOOL_SIZE-1; k >= 0; k--) {
if (pool[k].key == NULL) continue;
de = dictFind(server.db[pool[k].dbid].dict,
pool[k].key);
/* Remove the entry from the pool. */
if (pool[k].key != pool[k].cached)
sdsfree(pool[k].key);
bestkey = dictGetKey(de);
break;
}
}
优缺点
优点
LRU 实现简单,get、put 时间复杂度都为 O(1)
利用了 locality ,即最近使用的数据很可能再次被使用
能更快的对短期行为作出反应
缺点
long scan 的时候,会导致 lru 不断发生 evcit 行为。(数据库操作,从磁盘加载文件等,LFU 避免了该行为)
只利用了到了一部分的 locality,没有利用 最经常使用的数据很可能再次被使用(LFU 做到了,但是更慢,Log(N))
B 站源码解析之 LRUCache 实现
在这篇文章还没完稿的时候,看到了 B 站的 LRUCache 的源码实现,下面就顺便来分析一下。下面是对应的源码:
package lrucache
// 这里是 Node 节点的定义,中规中矩
// Element - node to store cache item
type Element struct {
prev, next *Element
Key interface{}
Value interface{}
}
// Next - fetch older element
func (e *Element) Next() *Element {
return e.next
}
// Prev - fetch newer element
func (e *Element) Prev() *Element {
return e.prev
}
// 通过这个结构体,猜测使用的是跟 Leetcode 经典实现类似的带头尾伪节点的链表实现。
// 后面发现实际上并不是
// LRUCache - a data structure that is efficient to insert/fetch/delete cache items [both O(1) time complexity]
type LRUCache struct {
cache map[interface{}]*Element
head *Element
tail *Element
capacity int
}
// New - create a new lru cache object
func New(capacity int) *LRUCache {
// 可以看到初始化 LRUCache 时,并没有初始化 Dummy head 和 Dummy tail
// 不过这样岂不是在操作过程中需要进行很多 if 判断,后面的代码也验证了这点。
return &LRUCache{make(map[interface{}]*Element), nil, nil, capacity}
}
// Put - put a cache item into lru cache
func (lc *LRUCache) Put(key interface{}, value interface{}) {
if e, ok := lc.cache[key]; ok {
e.Value = value
// 等同之前的 _insert_to_front
lc.refresh(e)
return
}
if lc.capacity == 0 {
return
} else if len(lc.cache) >= lc.capacity {
// evict the oldest item
delete(lc.cache, lc.tail.Key)
// 等同之前的 _delete_node
lc.remove(lc.tail)
}
e := &Element{nil, lc.head, key, value}
lc.cache[key] = e
// 因为没有 Dummy head 和 Dummy tail 而带来的不必要的 if 判断
if len(lc.cache) != 1 {
lc.head.prev = e
} else {
lc.tail = e
}
lc.head = e
}
// Get - get value of key from lru cache with result
func (lc *LRUCache) Get(key interface{}) (interface{}, bool) {
if e, ok := lc.cache[key]; ok {
lc.refresh(e)
return e.Value, ok
}
return nil, false
}
func (lc *LRUCache) refresh(e *Element) {
// 可以看到这里很多的 if 判断,严重干扰了代码的阅读逻辑。
if e.prev != nil {
e.prev.next = e.next
if e.next == nil {
lc.tail = e.prev
} else {
e.next.prev = e.prev
}
e.prev = nil
e.next = lc.head
lc.head.prev = e
lc.head = e
}
}
func (lc *LRUCache) remove(e *Element) {
// 可以看到这里很多的 if 判断,严重干扰了代码的阅读逻辑。
if e.prev == nil {
lc.head = e.next
} else {
e.prev.next = e.next
}
if e.next == nil {
lc.tail = e.prev
} else {
e.next.prev = e.prev
}
}
怎么说呢,有点失望。一开始在看到结构定义了 head 、tail时以为是使用了 Dummy head 和 Dummy tail 的经典实现,但是在初始化时发现没有初始化对应的 head、tail,以为是使用了一种未知的新方法,但是一看 refresh 和 remove 的逻辑,发现是通过大量的 if、else 来判断 corner cases。
而大量使用 if 会严重干扰代码的可读性和可维护性,具体可见 Applying the Linus Torvalds “ Good Taste ” Coding Requirement 这篇文章。
Golang/groupcache LRUCache 实现
跟网友的讨论中,有人又贴出了 google 的一版实现,在 Golang/groupcache 项目下。就顺便看了下对应的源码。
// Package lru implements an LRU cache.
package lru
import “container/list”
// Cache is an LRU cache. It is not safe for concurrent access.
type Cache struct {
// MaxEntries is the maximum number of cache entries before
// an item is evicted. Zero means no limit.
MaxEntries int
// OnEvicted optionally specifies a callback function to be
// executed when an entry is purged from the cache.
OnEvicted func(key Key, value interface{})
ll *list.List
cache map[interface{}]*list.Element
}
// Add adds a value to the cache.
func (c *Cache) Add(key Key, value interface{}) {
if c.cache == nil {
c.cache = make(map[interface{}]*list.Element)
c.ll = list.New()
}
if ee, ok := c.cache[key]; ok {
c.ll.MoveToFront(ee)
ee.Value.(*entry).value = value
return
}
ele := c.ll.PushFront(&entry{key, value})
c.cache[key] = ele
if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries {
c.RemoveOldest()
}
}
// Get looks up a key’s value from the cache.
func (c *Cache) Get(key Key) (value interface{}, ok bool) {
if c.cache == nil {
return
}
if ele, hit := c.cache[key]; hit {
c.ll.MoveToFront(ele)
return ele.Value.(*entry).value, true
}
return
}
关键点在于 container/list ,这是一个带头结点的循环双向链表,但是并没有暴露 root 节点,所以 google 的实现同 Leetcode 经典实现是一致的。 我还发了一个 issue 去询问为什么不采用类似 Python 的实现。官方的回答是目前够用,如果需要变更的话,需要 benchmark 的支持。理论上 Python 的实现在不断读取新数值的时候性能会好很多。
综合下来 Python 内置库 functools.lru_cache 的带头结点的双向循环队列的实现是最优雅的
Shidenggui
作品
归档
订阅
复制列表元素时 Python 做了什么
5 April 2019
缘起
而刚刚学习 Python 的人常常会遇到一个问题,如下
li = [[]] * 10
li
[[], [], [], [], [], [], [], [], [], []]
li[0].append(1)
li
[[1], [1], [1], [1], [1], [1], [1], [1], [1], [1]]
li[0].append(1)
li
[[1], [], [], [], [], [], [], [], [], []]
li = [[] for _ in range(10)]
li[0].append(1)
li
[[1], [], [], [], [], [], [], [], [], []]
一探究竟
最近在看 《Python源码剖析》,让我们看看 [obj] * count 在 Python 底层是如何实现的。
字节码
Python 所有的代码都会编译成字节码之后执行,而类似 [obj] * count 的表达式,编译成字节码之后为
import dis
def test(): return [obj] * count
dis.dis(test)
1 0 LOAD_GLOBAL 0 (obj)
2 BUILD_LIST 1
4 LOAD_GLOBAL 1 (count)
6 BINARY_MULTIPLY
8 RETURN_VALUE
具体指令实现
而 BINARY_MULTIPLY 指令在源码中的实现为
TARGET(BINARY_MULTIPLY) {
# 获取 count
PyObject *right = POP();
# 获取 [obj]
PyObject *left = TOP();
# 重点来了,在这里获取相乘的结果
PyObject *res = PyNumber_Multiply(left, right);
Py_DECREF(left);
Py_DECREF(right);
SET_TOP(res);
if (res == NULL)
goto error;
DISPATCH();
}
PyNumber_Multiply 实现
指令的核心便是 PyNumber_Multiply,Python 源码中对该函数的注解
/* This is the equivalent of the Python expression: o1 * o2. */
PyAPI_FUNC(PyObject *) PyNumber_Multiply(PyObject *o1, PyObject *o2);
原来 Python 中所有 o1 * o2 的操作最后都是通过该函数实现,而 [obj] * count 也不例外。
PyNumber_Multiply 的实现如下:
PyObject *
PyNumber_Multiply(PyObject *v, PyObject *w)
{
// ……
// v 为 [obj],w 为 count
// v->ob_type 就是 [obj] 的类型 list, 而 tp_as_sequence 是其实现的一组与 sequence 相关的函数集合。
PySequenceMethods *mv = v->ob_type->tp_as_sequence;
PySequenceMethods *mw = w->ob_type->tp_as_sequence;
Py_DECREF(result);
if (mv && mv->sq_repeat) {
// 因为 [obj] 定义了 tp_as_sequence->sq_repeat 所以最后会进入这里
return sequence_repeat(mv->sq_repeat, v, w);
}
else if (mw && mw->sq_repeat) {
return sequence_repeat(mw->sq_repeat, w, v);
}
return result;
}
而 sequence_repeat 仅仅是简单进行了一些类型检查,最后直接 call repeatfunc,也即外界传入的 sq_repeat
static PyObject *
sequence_repeat(ssizeargfunc repeatfunc, PyObject *seq, PyObject *n)
{
// 略过
return (*repeatfunc)(seq, count);
}
sq_repeat 到底是什么?
该函数在 list 类型的实现为
// list_as_sequence 实现了一组与 list 相关的操作
static PySequenceMethods list_as_sequence = {
// 比如 list 的长度
(lenfunc)list_length, /* sq_length /
// 这就是最终调用的函数
(ssizeargfunc)list_repeat, / sq_repeat */
…
};
list_repeat 实现源码
经过寻找,我们最后终于找到了最终实现 [obj] * count 的代码。
下面是 list_repeat 简化后的代码:
static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
…
PyObject *elem;
# np 指向新分配的 count 大小的 list 对象
np = (PyListObject *) PyList_New(size);
items = np->ob_item;
# 如果 [obj] 的对象大小为 1 的话,我们这里就是如此
if (Py_SIZE(a) == 1) {
# 获取 obj,即这里的 elem
elem = a->ob_item[0];
for (i = 0; i < n; i++) {
# 因为 elem 是指针,所以这里新的 list 的 items 里面包含的是原有的 obj 的引用,而不是复制
items[i] = elem;
Py_INCREF(elem);
}
return (PyObject *) np;
}
...
}
所以 [obj] * count 最后获得是包含 count 个 obj 引用的新 list
结尾
最后我们可以看到实际上最后实现该功能的函数是
obj->ob_type->tp_as_sequence->sq_repeat = list_repeat
Python 为什么经过那么多层的抽象呢?因为 C 本身并不支持 class,而 Python 本身却是万物皆对象,而基于 C 实现的 Python 自然就需要通过大量的抽象来模拟实现面向对象的功能。
结尾安利下 《Python源码剖析》,虽然书有点老了,而且基于 Python 2.5 ,但是 Python 整体的大框架并没有改变很多,还是非常值得一看。
Shidenggui
作品
归档
订阅
编程与数学(五):Trailing zeroes in factorial
13 January 2019
缘起
最近在看《编程之美》,里面有一小节是探讨 N! 的阶乘的尾部零的数量的问题。里面提出来一个规律即对于 N! 的二进制表示,则尾部的零的数量为 N - one_bits(N),即 N 减去 N 的二进制表示里 1 的数量,只需要 O(1) 的时间即可求解该问题。然后书里面举了一个例子,但是没有给出证明为什么这个规律成立。这么优雅的规律怎么能没有证明呢,它真的成立吗?
Why?
首先书里面指出 binary(N!) 尾部零的数量 = N / 2 + N / 4 + N / 8 …. 直到 N / (2^k) 为 0 为止,这个很容易理解,接下来比较关键的就是证明 N / 2 + N / 4 + N / 8 …. 直到 N / (2^k) 为 0 为什么等于 N - one_bits(N)。(这里的 / 是 C 里面的 / )
下面给出证明,如有错误欢迎指出:
结尾
优雅的结论背后一定隐藏着优雅的证明,我想这就是数学猜想的魅力所在吧!
Shidenggui
作品
归档
订阅
编程与数学(四):Check for integer overflow on multiplication
4 October 2018
缘起
最近刷了一些 leetcode 的题目,发现里面经常需要检测整数相乘是否溢出的问题,而答案给出的检测方法都比较特定而且不够方便。这让我想起了之前看 CSAPP 的时候,在第二章 Representing and Manipulating Information中有一道课后题,提供了一种检测乘法溢出的方法,简洁明了。因此在这里介绍下。
Check for integer overflow on multiplication
题目来自于 CSAPP’s Practice Problem 2.35:
int tmult_ok(int x, int y) {
int p = x * y;
return !x || p / x == y;
}
这里我们看到只要排除了 x 为 0 的情况后,如果 p / x != y 则溢出,否则即无溢出。这时候不禁要问一句
Why?
下面给出证明,在 CSAPP 答案的基础上按自己的理解稍做了一点修改:
结尾
判断的方法很简单,但是后面的论证却没那么容易。不过这便是不光知其然,还知其所以然的乐趣所在吧!
Shidenggui
作品
归档
订阅
编程与数学(三):Newton‘s Method
27 September 2018
缘起
前几天看到一道题目,是关于“对整数 n 开平方,求其整数部分”,解法用到了 Newton’s Method,因为之前刚刚学过,就顺便复习下什么是 Newton’s Method,为什么可以用于求解这道题?
Newton’s Method
本身是用于逼近函数零点的一种技巧。因为对没有求根公式的函数,求解它的零点是非常困难的,因此就发明了 Newton‘s Method 来逼近该函数的零点。具体方法如下图所示:
应用
至于为什么用于逼近函数零点的 Newton’s Method 会跟 “对整数 n 开平方” 有关代码实现如下:
def int_sqrt(n):
"""
»> int_sqrt(0)
0
»> int_sqrt(1)
1
»> int_sqrt(80)
8
»> int_sqrt(81)
9
»> int_sqrt(82)
9
"""
x_n = 1
x_n_plus_1 = (1 + n) / 2
while int(x_n_plus_1) != int(x_n): 原来的错误做法,具体见评论
while abs(x_n_plus_1) != int(x_n):
x_n = x_n_plus_1
x_n_plus_1 = (x_n + n / x_n) / 2
return int(x_n_plus_1)
如果是开 k 次方呢?
代码实现如下:
def int_sqrt_of(n, k=3):
"""
»> int_sqrt_of(26, 3)
2
»> int_sqrt_of(27, 3)
3
»> int_sqrt_of(28, 3)
3
"""
x_n = 1
x_n_plus_1 = (k - 1 + n) / k
while abs(x_n_plus_1 - x_n) > 0.01:
x_n = x_n_plus_1
x_n_plus_1 = ((k - 1) * x_n + n / x_n ** (k - 1)) / k
return int(x_n_plus_1)
Shidenggui
作品
归档
订阅
编程与数学(二):Geometric Series
16 September 2018
编程与数学(2): Geometric Series
缘起
前几天偶然看到一道题:A、B 打赌互相扔硬币,先抛出正面的人胜利,假设 A 先抛的话他赢的概率是多少? 当时跟同事讨论了下,结果是 2/3。感觉这题挺有意思的,就想详细研究下,所以有了这篇文章。
解法
从概率的角度看,第一个人可能赢在第 1、3、5、7 ….. 次,而每次赢必然是前几次抛硬币结果都为负,最后一次为正,因为抛硬币为独立事件,则可以知道赢的概率之和为:
还有呢?
如果有 3 个人或者说 k 个人打赌,那第一个人赢的概率是多少呢?
如果是 3 个人打赌,则第一个人可能赢在第 1、4、7、11 ….. 次,则在级数中对应的项次由两个人时的 2n+1变为 3n+1(注:n 从零开始),同理可得如果是 k 个人,则为 kn+1,则可以知道 k 个人时赢的概率之和为:
令 k=3,则 P(A) = 4 / 7。
如果 A、B 决定加点难度,改为掷骰子,先掷出 6 点的人胜,那第一个人赢的概率是多少呢?
同理可知,第一个人可能赢在第 1、3、5、7 ….. 2n+1 次,稍微不同是赢之前的那几次,他们只需要不掷出 6 即可:
如果是掷 t 面骰子,先掷出 t 点的人赢,那第一个人赢的概率是多少?
如果是 k 个人掷 t 面骰子,先掷出 t 点的人赢呢,那第一个人赢的概率是多少?
结尾
从上可以知道其实所有变化都只是 Geometric Series 的变种。这就是数学的魅力所在,从各种各样的形式中发现统一的模式,又将这统一的模式应用于无穷无尽的变化中。
枢始得其环中,以应无穷。 –《庄子·齐物论》
Shidenggui
作品
归档
订阅
设计模式与 easytrader (一):策略模式
17 August 2018
前言
之前一段时间,easytrader 上用户报了很多 issues,基本都是关于客户端软件无法获取持仓的错误。所以最近就抽了段时间解决下,里面用到了策略模式,就想顺便复习下设计模式。
这里介绍下相关的上下文,easytrader 是一个模拟证券客户端操作的Python类库,基本就是一个定制版的按键精灵,支持银河、华泰等公司以及通用版客户端(支持多家券商),能进行的操作有买卖、获取持仓等。
Old Design
issue 里面的错误是关于获取持仓 get_position 这个接口的,画成类图的话如下所示(省略了一些上下文无关的信息):
这边有问题的是获取持仓这一步。原来客户端的持仓保存在Grid 中,类似网页端的 table,然后 easytrader 通过Ctrl + C 的方式复制内容到剪切板,再解析剪切板的内容获取持仓:
示例代码:
class ClientTrader:
def get_position(self):
# …
grid_data = self._get_grid_data()
# …
return position
这个逻辑是所有客户端通用的,因此 YHTrader 和 HTTrader 都是继承的父类 ClientTrader 中的默认方法。
Break
但是前段时间银河和通用客户端的一部分券商通过更新封杀了通过剪切板获取 Grid 数据的方式,导致原有的 get_position 失效。这时在 issue 里有开发者提出可以通过在 Grid 上右键将对应内容保存为 xls 文件再解析保存的文件获取持仓,并给出了示例代码。
Question
这时候的问题就是怎么将通过 xls 获取持仓的策略和原有的通过剪切板获取持仓的策略整合到代码里,修复银河客户端和一部分通用客户端无法获取持仓的 Bug。
解决方式
HardCode
在 ClientTrader 类中实现 get_position_by_copy 和 get_position_by_xls 的方法,并在 get_position 进行 hardcode,然后 YHTrader 通过继承的方式默认使用 xls 方式,示例代码如下:
class ClientTrader:
def get_position(self, strategy=‘copy’):
# …
if strategy == ‘copy’:
grid_data = self._get_grid_data_by_copy()
elif strategy == ‘xls’:
grid_data = self._get_grid_data_by_xls()
else:
raise NotImplementedError(f’Stratege {strategy} not implenmented’)
# …
return position
def _get_grid_data_by_copy(self):
# ...
def _get_grid_data_by_xls(self):
# ...
class YHTrader(ClientTrader):
# 修改银河的方法默认为 xls 策略
def get_position(self, strategy=‘xls’)
# …
优点
简单粗暴
对 api 的改动较小
缺点
如果获取 position 的方式继续增多,get_position 中会有很多面条式的代码,不好维护。
修改持仓策略的话需要修改ClientTrader 类,而这个类作为父类,修改它影响范围较广,违背了对扩展开放,对修改关闭的开闭原则。
污染ClientTrader和 get_position 的代码结构,导致阅读代码时有很多无关的代码细节干扰思路
Inheritance (基于继承)
如果我们基于继承实现的话会怎么样,因为通用客户端从原有的仅支持一种策略变为支持两种,此时就需要基于 copy 策略的父类实现使用 Xls 策略的子类,而银河客户端从 Xls 策略子类继承,这里同时需要修改获取 Trader 的工厂方法,提供获取Xls类的方法。示例代码如下:
class ClientTrader:
def get_position(self):
# …
grid_data = self._get_grid_data()
# …
return position
def _get_grid_data(self):
# 默认实现基于 copy 的 grid_data 获取逻辑
class ClientTraderWithXls(ClientTrader):
def _get_grid_data(self):
# 实现基于 xls 的 grid_data 获取逻辑
class YHTrader(ClientTraderWithXls):
pass
class HTTrader(ClientTrader):
pass
def get_trader(broker):
if broker == ‘client_trader’:
return ClienTrader()
if broker == ‘client_trader_with_xls’: # 新增获取 xls 策略子类的方法
return ClientTraderWithXls()
# …
优点
看起来将 xls 的策略逻辑抽取到了子类中,比较独立
不需要修改原有的 client_trader 类
不需要修改原有的 api 接口
缺点
如果其他接口需要多种策略的话,子类的策略组合数量会出现爆炸式上升。比如获取余额也需要新增一种策略的话,加上获取持仓的两种策略,则要实现四个子类才能涵盖所有的组合。
那有没有更好的方法呢?自然是有的,那就是设计模式中的策略模式。
Strategy (策略模式)
根据《Design Pattern》,策略模式的目的是:
定义一组封装内部细节并可互换的算法,使得客户端跟它们使用的算法解耦。
而为什么不选择将对应的算法直接嵌入类中或通过继承实现呢?除了我们上面所讲的几点外,书中还列出了直接 hardcode 的带来的一些其他问题:
将算法细节包含在类内实现,会使得类本身复杂度上升和代码膨胀,导致维护困难,尤其需要支持多个算法的时候。
想象下如果我们需要支持 10 种获取持仓的算法,第一种 HardCode 的实现会导致类基本丧失了可维护性。
可能需要在不同的时间使用不同的算法,而且有时不需要同时支持多种算法。
HardCode 本身允许通过参数调用不同的方法,但是也同时将所有算法的代码包含在内。
类继承的方式虽然只包含对应算法的代码,但是却不允许用户切换算法实现。
当类本身包含了大量算法实现的时候,修改或者变更已存在的算法都会非常困难。
将算法本身包含在类中暴露了太多细节。而一般来说,程序员会滥用这些暴露的细节使得类本身的行为变得非常脆弱。当修改算法时需要获知很多不必要的上下文细节。
而为了避免这些问题,我们可以定义一些将算法细节封装起来的类,它们具有统一的接口。而基于这样的选择封装的类,我们称它为一个Strategy。
这时就让我们按照策略模式的方式修改我们的代码如下 :
import abc
class IGridStrategy(abc.ABC):
“““获取 grid 策略接口定义”””
@abc.abstractmethod
def get(self):
"""返回 grid 数据"""
pass
class Copy(IGridStrategy):
def get(self):
"""实现基于 Copy 的逻辑"""
pass
class Xls(IGridStrategy):
def get(self):
"""实现基于 Xls 的逻辑"""
pass
import grid_stragies
class ClientTrader:
# The strategy to use for getting grid data
grid_strategy = grid_stragies.Copy
def get_position(self):
# ...
grid_data = self.grid_strategy(self).get()
# ...
return position
class YHTrader(ClientTrader):
grid_strategy = grid_stragies.Xls
class HTTrader(ClientTrader):
pass
import grid_strategies
client_trader = ClientTrader()
client_trader.grid_strategy = grid_strategies.Xls
优点
将相关的策略细节都抽取到了另外一个文件中
增加策略时只需要保证接口一致且不需要修改原有的 client_trader 相关类
可以通过修改类的 grid_strategy 属性来动态切换策略,避免多种方法需要多种策略时组合爆炸的问题。
缺点
允许动态切换类导致的性能轻微下降。如果不需要动态切换的话可以通过 mixin 的形式实现。
适用场景
策略模式适合于以下场景:
许多相关类仅仅在行为上有所区别。策略模式提供了一种方式允许配置一个类选择多种行为中的一种。(而不是在一个类中实现所有算法或者继承许多类而仅仅为了覆写不同的算法)
需要使用一种算法的多种变种。策略模式允许你基于类的结构定义多种轻微不同的算法。
算法的内部细节不需要暴露给客户端。使用策略模式避免暴露内部复杂以及特定的数据结构。(提高代码的可读性和可维护性)
一个类定义了很多行为,然后在实现中通过条件语句来切换(见 HardCode 的实现)。此时可以通过将相关条件分支移动到对应的策略类中消除条件语句。
实现细节
strategy 如何访问 context
context 将自身的引用传递给 strategy,适用于 strategy 和 context 关联较为紧密的情况或者参数较多的情况
class Context:
strategy = ConcreteStrategy1
def do_something(self):
self.strategy.do_something(self)
context 将所需数据通过参数传递给 strategy,适用于需要解耦 strategy 和 context 或者参数较少的情况
class Context:
strategy = ConcreteStrategy1
def do_something(self):
self.strategy.do_something(self.arg1, self.arg2)
由 client 选择 strategy 传递给 context,适用于需要在运行时变更 strategy 的情况
class Context:
def do_something(self, strategy):
strategy.do_something(self)
Context().do_something(ConcreteStrategy1)
第一种方法更加灵活,但是暴露了 context 本身过多的信息给 strategy,使得策略和 context 紧耦合。
第二种方法如果需要新的参数,则需要变更 strategy 类的接口,这时候需要 strategy 类本身设计的较为合理。
第三种方法提供了 client 在运行时变更 strategy 的能力。
缺点
虽然策略模式有很多优点,但是不可避免的也会有一些缺陷,正如”没有银弹“所说的,掌握设计模式的关键便是理解它们对应的 tradeoff,如此才可以在正确的时间选择正确的模式。
如果 client 需要切换 strategy 的话,它必须理解对应的 strategy 的区别。
context 和 strategy 之间的交互带来的额外成本。如果 strategy 设计不合理的话,context 传递的一些参数可能在某些 strategy 中永远不会被用到。
类以及对象数量的膨胀。这在设计模式中是很难避免的,毕竟设计模式的第二原则就是 Favor object composition over class inheritance.
实际案例
django-rest-framework
rest_framework.views.APIView 就大量使用了策略模式以使得用户在编写对应的 APIView 时可以获得最大的灵活性
class APIView(View):
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
Shidenggui
作品
归档
订阅
编程与数学(一):Logistic Differential Equation
24 July 2018
最近在准备尝试复活我很久前写的网文推荐系统,把 2 年前推送到 daocloud 的 Docker 镜像重现部署到了现在新的服务器,并暂时放到了这个域名。
这时候发现一个问题,里面的相似度计算是基于余弦算法,一般同作者的两本书 A、B 之间的相似度在 30% ~ 35% 左右,位于 35% 到 100% 间的极少,而 20% 左右已经不太像了。这时候就想找一个 mapping 算法,希望能满足以下特征:
结果值随着原始相似度的提高剧烈上升,在 30% 时候能映射到 90% 的相似度
结果值最大为 100%,因为最多就 100% 相似
在 20% 相似度左右的时候结果值较低
理想中应该是个 S 型的曲线。根据第一个特征想到了基于指数增长的函数:
但是它不满足有极大值的限制,这时候就想到了常用于描述人口增长的 Logistic Differential Equation:完美满足了前期指数级上升,又存在一个极限 L 的情况。但是它也有一个不太完美的地方,就是当相似度为 0 时它的值不能为 0 , 所以只能假设相似度为 0 时方程值为 1。
接下来就是解方程获取精确的公式了:
求解后就是试着画图和输入 0 到 100 之间的数值感觉符不符合预期了,结果如下 ,图中灰线交点为(30, 90):
精确值如下:
0 1.00000
5 3.03817
10 8.85875
15 23.16620
20 48.32840
25 74.36770
30 90.00000
35 96.54200
40 98.85850
45 99.62910
50 99.88010
55 99.96130
60 99.98750
65 99.99600
70 99.99870
75 99.99960
80 99.99990
85 100.00000
90 100.00000
95 100.00000
100 100.00000
感觉还不错,参数可能还需要根据实际情况微调。如果有更好的 mapping 函数或者方法,欢迎大家留言。
Shidenggui
作品
归档
订阅
跑步与数学
21 July 2018
缘起
最近有一天晨跑的时候,突然想到了刚学的微积分里面的 The Mean Value Theorm。因为记得书上说这是微积分中最重要的定理之一,就心血来潮想回忆下这个定理是怎么推导出来的?也因此有了一段很奇妙的体验。
正文
The Mean Value Theorm 简述如下:
证明如下:
而此时我们可以回想起 Rolle’s Theorem:
代入上式,则
这里不由得要问,为什么Rolle‘s Theorm成立呢?证明简述如下:
那为什么当 f(x) 不为常量时它的极值导数一定为 0 呢?
尾声
这时候我没有疑问了,心满意足了,而跑步也临近结束,此时我内心的满足感无以复加,仿佛一个小孩终于得到了自己想要的东西。
那这些数学的推论跟跑步有什么关系呢?因为只有在跑步的时候才能放空自我,让思维漫无边际的漫游,飘荡在这世界中,欣赏人类纯粹的智慧成就。
我突然理解为什么那么多人醉心于数学的优雅和精致。在现实生活中,有很多问题你无法获得解答,但是在数学中,对于你感到疑惑的定理,只要你有好奇心,你总是可以问到你心满意足为止。
我想,这是不是所谓的苏格拉底式提问法呢?一直追问下去,直到你的好奇心满足或者回答不了发现自己的盲点为止。
最近我偶然看到了一个费曼关于 why 的视频,里面也谈到了类似的思考方式。我想用一句话总结我的感受:
Ask why! Ask why! Ask why until you’re satisfied!
Shidenggui
作品
归档
订阅
Talk is cheap. Show me?
13 July 2018
For programmer: Talk is cheap. Show me the code.
For math: Talk is cheap. Show me the proof.
For physics: Talk is cheap. Show me the prediction.
For life: Talk is cheap. Show me the action.
For meditation: Talk is cheap. Show me yourself.
For yoga: Talk is cheap. Show me the practice.
Shidenggui
作品
归档
订阅
读《Why We Sleep》
15 June 2018
缘起
之前有段时间睡眠不太好,就想找本睡眠相关的科普书看下。毕竟想改善某些事物的话,最好能多了解它一点。在查看了豆瓣、Amazon、Goodreads 上面的评分后,选择了《Why We Sleep》。主要有以下几点:
评分不错
作者是哈佛专门研究睡眠的教授,几十年的科学研究经验
年份较近,对于快速发展的生物医学来说,越近说明知识靠谱的概率会高一点。
看了之后虽然感觉作者文笔一般而且比较啰嗦,不过在科普方面还是挺不错的。
概览
书的内容按照以下几章分布
是什么控制着我们的睡眠节律?
睡眠本身由哪些阶段组成?
影响我们睡眠节律的因素有什么?。
睡眠以及睡眠不足对人类来说意味着什么?探讨了为什么大自然会进化出睡眠,而这一天赐的机制到底是为了什么而生?
睡眠不足对社会的影响?这章比较无聊,不感兴趣的可以跳过
如何改善我们的睡眠?
是什么控制着我们的睡眠节律?
昼夜节律 / circadian rhythm
古时候的人们以为是太阳控制着睡眠节律,毕竟在没有人造光源的时代,大家都过着日出而作、日落而栖的生活,而随着科学时代的到来,人们的观念也逐渐发生了改变。
欧洲中世纪的人发现有些植物的枝叶会随着跟随太阳的轨迹而移动,他们以为是受到太阳光照的影响。直到 1729 有位法国科学家 Jean-Jacques d’Ortous de Mairan 针对 Mimosa pudica(含羞草)做了一个精妙的实验。因为含羞草本身不止枝叶会有向阳性,而且晚上会把叶子收起直到太阳升起时再展开,也因此被达尔文戏称为“sleeping leaves"。当时的人们以为是受太阳光照的影响,而他将含羞草密闭在一个黑盒中 24 小时,在中间打开小盒窥视发现植物的行为还是跟它在受光照时一样,因此猜测植物内部有独立的生物钟,而不仅仅是太阳光照的奴隶。而其后过了 200 年,才证实人类也存在着类似独立的昼夜节律。200 年,现在的我们是真正站在巨人的肩膀上,沐浴着智慧的光芒。
人类昼夜节律的发现要归功于两位勇敢的科学家。1938 年芝加哥大学的两个科学家 Nathaniel Kleitman 和 Bruce Richardson 准备进行一个疯狂的实验,带满六周的口粮和两张高脚床在世界上最深的洞穴之一的 Mammoth Cave 中居住并记录人类的睡眠周期。因为洞穴深处完全隔绝了阳光,是一个非常理想的实验地点。他们想要回答的是这样一个问题:当人类脱离自然光照后,他们的体温、睡眠周期同正常情况下有什么区别?他们最后在洞穴中居住了 32 天。在此间还有一件趣事,他们为了防止被洞穴中的生物侵袭,将床的四只脚都放在水桶里,因为大部分动物都不会涉水,也就避免了在伸手不见五指的深处床上爬满各种古怪的生物。32 天的付出带来的是人们对睡眠的全新认知:
人类存在内源性的昼夜节律
人类的昼夜节律具有一定的稳定性而且比 24 小时稍长。
现在的科学家通过大量实验,已经确认了成年人的昼夜节律大概是 24 小时 15 分钟,也就是说在没有外界参照的情况下,人类本身就会每天睡的越来越迟。这也解决了我很久以前的一个疑惑,为什么我以前经常每天会迟睡一点,然后不知不觉就熬夜到很迟了,原来人本身的昼夜节律就倾向推迟人本身的睡眠时间。
而我们之所以能维持 24 小时的生活节律是因为外部环境的影响。其主要作用的是太阳光照,但是定时摄入食物,温度的周期性变化,定时运动甚至人际之间的交往都有一定的作用,大脑会据此轻微的修改我们的昼夜节律。
昼夜节律的发现到现在己经有70多年的历史了,科学家也对它有了越深入的理解。昼夜节律本身由大脑内部命名为 suprachiasmatic(“supra, meaning above, and chiasm, meaning a crossing point of the optic nerves”. ) 的神经细胞控制。这群细胞的数量只有 2w 个,在大脑 1000 亿左右的神经元中占比微不足道,但却对我们的生理系统有着重要的影响。昼夜节律本声是由一系列的生理反应组成,包括体温周期性的变化、人类周期行的苏醒和睡眠等。人类的体温在凌晨 4 点左右逐渐升温,到下午 3 时左右达到顶峰,然后逐渐下降。所以一般下午 3 点作用是人体能的巅峰期。然后到了晚上脱离了光照后会逐渐分泌褪黑素,褪黑素作为吹响入睡的号角声,会导致一系列诱发睡眠的生理反应产生,伴随夜晚体温降低人就入睡了。这时到了早上受到阳光照射使褪黑素停止分泌加上体温的逐渐上升,人就从睡眠状态醒来。这就是昼夜节律在人的睡眠中所起的作用。
每个人都有自己的昼夜节律。据统计有 30% 的人为 “morning types”,即早起型,30% 为 “evening types”,即夜猫子型,剩下的人介于其中之间。
睡眠压力 / sleep pressure
为什么人们都说一年之际在于晨,白天刚起床的时候人会比较清醒呢?为什么我们熬夜到后面会越来越困,而如果你通宵到快天亮时反而没那么困,而到了第二天下午又很想睡觉?为什么喝了咖啡后会很精神,但是效力过去后又会非常困?这些现象的背后是什么生理机制在其作用呢?
随着现代医学的发展,人们发现人类大脑中有一个腺体,叫做 suprachiasmatic, 会分泌一种 adenosine(中文名腺苷,下面简称为睡意激素) 的物质,而这种激素正是人类产生睡意的来源。你感到困倦、昏昏欲睡都是这种激素在起作用,科学家把这种机制称为 sleep presure。而它跟昼夜节律一起正是决定人类睡眠的两大因素。
睡意激素在人醒着时开始分泌并积累在人的血液中,通过跟大脑中特定的受体结合而起作用,不断的对人施加睡眠的压力,而只有在睡眠时这种激素才会被从身体里清除。
而咖啡提神的原因也是因为它比睡眠激素有着更强的跟大脑中受体结合的能力。当人摄入咖啡因时,它会将睡眠激素从受体上挤开而自己跟受体结合,但咖啡因却不会使你产生睡意,因此人会感觉精神起来。你精神并不是咖啡提神,它只是将你原有的睡意暂时消除了。但是此时睡眠激素并没有被从身体里清除,还不断堆积在身体中,而更糟糕的是它的受体被咖啡因霸占了,导致它在身体里的浓度比平常更高,而一旦咖啡因的效力过去,高浓度的睡眠激素又会跟受体结合,人会感觉到比之前更困,这种现象被称为 ““caffeine crash”。
而咖啡因不只在存在于咖啡中,在茶、运动饮料、巧克力、冰淇淋中都广泛分布。而且咖啡因的在血液中的代谢半衰期在 5 到 7 小时左右,很可能你早上喝的一杯咖啡,到晚上还在你的血液中残留着 25% 的咖啡因,此时你很难将你晚上的难以入眠跟白天的一杯咖啡关联起来,咖啡因成了一个隐藏的黑手。
而早晨人会比较清醒是因为此时人体内的睡眠激素浓度随着昨晚的睡眠浓度不断下降,而刚起床时受昼夜节律的影响体温处在逐渐上升中,因此整个人都比较精神。
而熬夜到早晨没有那么困也是因为虽然睡眠激素还在积累中,但是到早上受昼夜节律的影响人的体温也处在逐渐上升中,所以抵消了一部分睡眠激素的影响,而到了下午体温下降加上高浓度的睡眠激素积累,便遇到了睡意的大爆发。
具体可以参照下图
睡眠本身由哪些阶段组成?
睡眠本身由两个阶段组成,分为快速眼动期(Rapid eye movement sleep: REM)和非快速眼动期(Not rapid eye movement sleep: NREM)。
人类以前并不知道由快速眼动期的存在,直到有一个科学家偶然发现为止。
快速眼动期人的脑电波比清醒时还活跃,而在非快速眼动期人的脑电波则比较和缓。人类做梦的阶段通常就发生在快速眼动期阶段。通常快速眼动期和非快速眼动期会交替出现,它们一起会组成大概90分钟的一个周期,通常在睡眠早期以 NREM 为主,在睡眠晚期,临近起床时以 REM 为主。
所以如果早起导致睡眠不足的话会损失一大部分的 REM。
影响我们睡眠节律的因素有什么?
咖啡因
具体参上,很多人睡眠不好其实是受到很久之前摄入的咖啡因到了晚上睡觉时还残留在身体里的影响,并不是真正的失眠。
酒精
人们认为喝酒助眠。作者在书里面驳斥了这个理论,喝了很多酒的人看起来是睡着了,但是通过监测他们的脑电波可以发现,说他们处于睡眠状态,不如说是“麻醉”状态。而“麻醉”状态本身并不能像真正的睡眠一样带来种种益处,只是一种自我欺骗。
温度
科学家曾做过实验,研制了一种可以控制温度的衣服给受试者穿着,然后通过调控衣服的问题来观察受试者的入睡时间,发现 18 度左右是比较适宜的入睡温度。低于 13 度或者过高的温度都会导致入睡时间延长。科学家提出的理论是人类起源于非洲,古时没有衣服都是裸睡,因此比较适宜非洲夜间 18 摄氏度左右的温度。
这边也解释了为什么晚上洗澡会比较助眠,洗完澡后身上残留的水珠蒸发时会带走热量,导致平均体温的降低,因此人也更容易入睡。
光照
人的昼夜节律会受到一部分光照的影响。科学家发现晚上只要经过灯光几分钟的光照就可以将褪黑素的分泌推迟 1 小时左右。而褪黑素正是人类睡眠过程开始的发令枪,由此也会干扰到人类睡眠。而且现代 led 灯中富含对人的干扰更严重的蓝光,不像以前的白炽灯接近太阳光,因此现代无处不在的光污染也会干扰人类正常的睡眠过程。推荐有条件的换用白织灯。
噪音
这个我觉得没什么好说的。不过科学家发现睡眠时播放一些特定的音乐可以对人的睡眠产生一定的影响,比如增强或者减弱对某些事的记忆里。
睡眠以及睡眠不足对人类来说意味着什么
我以前只是觉得睡眠是为了恢复人类白天的劳累所丧失的精力,并不清楚睡眠本身到底有什么具体的意义,因此自然也不知道睡眠不足到底有什么坏处。
作者首先探讨了为什么会有睡眠这种现象。睡眠占了人类一生中 1/3 的时间,而且在睡眠时人失去了对外界的感知,基本上丧失了自卫能力。在残酷的自然法则下,为什么人或者说大部分的动物都进化出了睡眠这个机制呢?睡眠本身一定有着极为重要的理由才能导致它能在那么多年的生物演化史中经受达尔文进化论的冲刷而屹立不动。
那睡眠本身到底有什么作用呢?虽然现代科学还没有揭开睡眠的全部秘密,但也产生了不少的收获,而这也是作者着力阐述的重点。
不睡眠会死
有些人得了一种特殊的病症,导致难以入睡,而且现代的一切药物都对他们不起效。在医学史上少数几个患有这个病症的人都在几个月的睡眠缺失后走向了死亡。
睡眠对记忆的作用
人在白天接受了很多信息,而睡眠就是大脑在晚上深加工这些信息的时候。哪些信息是没有意义需要丢弃的?哪些信息是最近可能用到的需要暂存起来的?哪些记忆非常重要需要长期记忆的?哪些记忆跟以前的记忆有关联可以联系起来的?这便是睡眠所做的事情。
睡眠不足会导致记忆力下降相信都可以理解,而睡眠充足会导致记忆力上升却是很少听到的理论。科学家找了一些受试者进行实验,一部分人在记忆一定量的单词后休息一段时间,但是不能睡觉,而另一部分人在记忆单词后进行几小时的短暂睡眠,然后测试他们对单词的记忆程度,有睡眠过的人对单词的记忆力远超过不休息的人。而重复这个实验,只是受试者变成晚上睡眠 6 小时和睡眠 8 小时两组,也可以得出同样的结论。
我常常背单词的,因此有更深的体会。有些单词你但是怎么都背不住,但是睡了一觉,在看到它时你自己还没意识到,单词的意思就自然而然出现了,就像魔法一样。
睡眠对理解力的提升
在 REM 阶段,人会做梦。而这也是大脑在不断对记忆进行深加工的阶段,大脑会将过去的储存的知识跟当前保存的记忆进行联想,看能不能找出深层次的关系。如果背过单词的应该可以理解,如果一个单词能跟一幅画或者一个动作、声音、感觉关联起来,那这个单词想忘记都比较困难。
大脑的机制也是类似,如果能找到记忆之间的关联性,则保存记忆的成本就会大大下降。而许多科学家也有对一个问题百思不得其解,但是某天做梦却突然梦到了解决方案。比如元素周期表的发明和 DNA 双螺旋结构的发现。而发现、建立、创造事物之间的联系,是创新和理解力的源泉之一。大脑作为一个网状结构,如果你能在其中建立更多关联,你的整个网络会更健壮,而当你遇到问题的时候也因此能激发大脑更多部分参与思考、联想、推理、理解。
睡眠减轻压力
在清醒的时候,人体内会产生大量的代谢废物和相关的压力激素。毕竟谁能在有意识的时候毫无压力、忧虑呢?而科学家观察到这些累积的废物大部分都会在睡眠时得到清除。睡眠不好的人相对脾气暴躁也更加容易抑郁,因为之前的压力激素和其他废物都没有得到合适的处理,不断在他身体里累积。
睡眠治愈心理创伤
人们常说时间会抹去一切伤痛,而这又是怎么实现的呢?正是通过一次又一次的睡眠,大脑对过往痛苦的记忆进行重加工,减轻了人每次回忆旧事时的心理创伤。而患有 PTSD 的人往往睡眠不好,因为他们这一机制受到了破坏,因此每次他们想到旧事就仿佛真的是旧事重现,而不是像大多数人一样一笑了之。
睡眠不足对社会的影响?
书里面举了一个美国夏令时和冬令时切换的时候人们会少睡或者多睡一个小时,导致当日全美交通事故发生波动的例子。
同时反对现在学校上学时间越来越早的变化,因为早起会干扰到深度 REM 的相关的睡眠质量,导致记忆力和理解力的下降。
同时建议公司上班时间更加的弹性化以便个人能根据自己的生物钟合理安排最高效的时间用于工作。
如何改善我们的睡眠?
书的结尾谈到了一些改善睡眠的办法,以下逐条列举:
固定睡眠时间,即起居有时
卧室不要摆放时钟和电子设备
除了睡觉不要呆在床上
在床上躺了 20 分钟还没有睡着的时候,起床做一些舒缓的事情,等有睡意了再上床
尽量使用白织灯,减少 Led 蓝光的影响
不要使用闹钟,如果不使用闹钟不能保证定时睡眠的话可以使用
不要摄入任何含咖啡因和酒精的制品
如果以上只能选一条做的话,选择第一条:固定睡眠时间
书里面还谈到对一些有睡眠障碍的人,比如很迟才能睡着,或者很早就醒了睡不着的,可以尝试使用 “Cognitive Behevier Theropy”,做法也非常简单:
不到非常困不要上床,如果上床不能马上睡着就起床
醒了要是短时间睡不着起床
非睡觉时间不要待到床上
如果晚上睡眠质量不好,白天不要进行任何小憩
可以看到更上面的建议非常类似,但是有一点重要的区别,就是“不到非常困不要上床”,书里面谈到这是为了积累足够的睡眠压力,在刚开始受试者的睡眠时间会缩短,但逐渐会慢慢恢复正常时间,并且他们的睡眠质量也得到了提高
结尾
所以作者在一开始就感叹,睡眠是大自然赐给人类最宝贵的礼物之一,是真正的“灵丹妙药”,更美妙的是你每天都有机会享受。可惜很多人不懂得珍惜,他们不知道他们失去了什么?
书中的建议我实行之后确实改善了我的睡眠质量,入睡时间变短而且睡眠质量得到提升。更重要的是它让我理解了睡眠到底是怎么一回事,而我们又怎么看待这占我们生命三分之一的睡眠?毕竟“知行合一”,真知方能真行。
Shidenggui
作品
归档
订阅
读《自控力》
30 May 2018
缘起
《自控力》是我很早以前买的一本书,但是在我最需要自控力的时候却没有意志力去阅读这本书,真是讽刺!之前把公司的书架都扫了下,感兴趣的基本都读了,然后就想到了这本买了大半年还尘封的书,花了几天看完了,感觉还不错,因此就写一篇读书笔记总结下。
概览
作者开篇就提出“自控力跟肌肉类似是一种生理力量,可以通过科学合理的方式得到锻炼和增强”。
如果有人问你“你觉得多久能练出一身肌肉?”,恐怕回答肯定不是几天、几周,至少也是几个月甚至好几年的时间,而且需要科学的方法、坚持不懈和以及付出艰苦的努力,而且在这个过程中,肯定会遇到不少的挫折、困难。但是我以前对“自控力”却有一种幻觉,总觉的只要我下定决心,我的内心就会发生神奇的变化,自此之后我就会充满“自控力”,但是实际上这只是一种幻觉,当这种幻觉在现实中破灭时人反而更加低落。
一旦人认识到“自控力”作为一种生理力量而存在,并不存在任何的神秘之处,自然就能从更加科学合理的角度来看待”自控力“不足的问题,无非是疏于使用而导致的力量不足。而如何理解、认识并增强”自控力“就是本书要阐述的话题了。
什么是自控力?
作者认为“自控力”有如下属性:
类似肌肉,在长时间使用后耗竭,需要一定的时间恢复。也可以通过一些科学合理的方式得到锻炼和增强
“自控力”具有生理学基础,是一种复杂的生理化学反应
人通过认识“自控力”的生理学基础和心理学现象可以更好的处理跟“自控力”相关的现象
自控力本身是有三部分组成:“我要做”,“我不要”,“我想要”。
我要做:当下指导我们行动的直观驱动力,比如现在我要刷牙、我要吃饭。简单对应人类的”欲望“。
我不要:我不想吃垃圾食品、我不想熬夜。简单对应人类的”恐惧“。
我想要:人类更高层次的欲望,比如我想成为一个科学家、我想探索人生宇宙的奥秘、我想度过一个有意义的人生。可以简单的对应人类的”好奇心“或者”理想“。
为什么要自控?
因为我有目标,而我的目标跟我当前的行动不匹配,我需要改变我目前的行动以使它能跟我的目标相契合。没有目标,自然也不需要自控,因为你都不知道你想成为什么样的人,自控什么呢?但是人没有目标真的好吗?
Where there is no vision, the people perish –《Proverbs 29:18》
自控力的生理学基础是什么?
自控力的来源是人类的大脑皮层,如果人类没有后天发展出来的大脑皮层,现在不过是跟动物一样随着自身的欲望而行动。当人类的欲望中心看到某些能引发人内心原始欲望的事物时,人的内心就会产生一种冲动,而此时人的大脑皮层就会介入,从理性的角度评估这种冲动的合理性,或允许或抑制。
在以下几种情况下,失控会发生:
大脑皮层的控制能力减弱,导致无法合理抑制内心冲动。最常见的就是摄入酒精了,不然怎么说“酒能乱性”呢?
内心的冲动过于强烈,超过了大脑皮层当时能抑制的程度,导致失控。比较常见的是玩游戏或者手机超时、长期节食之后接触食物等。
什么情况下容易发生失控?
盘点我个人的情况,可以发现一下几种情况
早上刚睡醒的时候自控力比晚上下班后强很多,基本上安排在早上的计划大部分都可以正常执行,而安排在晚上的计划就难说了。
某段时间想控制的事情太多,到最后耗尽自控力,完全无法控制自己的行为。
影响自控力的因素有哪些?
主要分为生理因素和心理因素:
生理状态:任何影响健康的因素某种程度上都会削弱人的自控力,比如睡眠不足、饥饿、疼痛、疲倦、愤怒。
心理状态:人是否处于长期的压力中、精神状况是否稳定等以及浏览一些充满负面报道的新闻、评论、文章。
道德许可
当你做善事的时候,你内心感觉良好,然后放纵自己的内心冲动。大部分觉得自己品德高尚的人,都不会质疑自己内心的选择,他们会想“我已经那么好了,应该给自己一点奖励”,而忘记了他们真正的目标。书中例子:和那些不记得自己做过善事的人相比,有行善经历的人在慈善活动中捐的钱要少 60%。
关键不是我们以前做了什么,而是我们现在做的是否有助于我们真正想要实现的目标。“不要把支持目标实现的行为误认为是目标本身“。
应对方法:取消许可,牢记理由。
虚假希望综合症
作出改变的决定,内心获得满足感,然后实践遇到挫折,引发内心的罪恶感、抑郁和自我怀疑,这时大部分人都会放弃努力。因为下决心是最容易的,但是往后的实践就越来越难了。
如何锻炼自己的自控力
这时我最感兴趣的地方,毕竟不实践,终究都只是空想。书里提了好几种方法:
任何有益于身体健康的事情
包括均衡的饮食、适当的运动、良好的睡眠,自控力是一种生理现象,自然也可以从一切有益于身体健康的行为中获益。额外比较有效的还有冥想,而且有时候冥想感觉糟糕,反而有助于自控力的养成。
每日锻炼法
核心思想是任何事情都可以通过长期锻炼得到增强,可以说是 growth mindset 的一种变体。具体方法是针对“我想要”、“我不要”两个类别设置一个小任务,每天坚持。我针对“我想要”设置的是“每日扔一件闲置的东西”,具体执行情况可见"断舍离之公式法"这篇文章。个人感觉还是挺有用的。
A small time can make a big different.
自我宽慰法
很多人在自控失败后往往会陷入更深的绝望,而为了摆脱绝望往往又会去做一些令自己失望的事情从而进入恶性循环。书里提出为了摆脱这种“绝望之螺旋”,在最初自控失败时,不要对自己进行严苛的批评,而是要安慰自己。想象你的朋友遇到了一件很糟糕的事情,像你寻求安慰时,你会对他说“这是你应得的,你活该如此,你一辈子都不可能摆脱这种情况,你只配自暴自弃”吗?而很多时候人就是这样对待自己的,而后果也可想而知。人不要虐待自己。
至于降低对自我的要求和批评是否会导致自控力的滑坡,书里通过一些实验表明并不会发生这种事情。很多事情遇到挫折是很正常的,但是怎么处理挫折在某种程度上对结果却有绝对性的影响。
书中谈到众多研究表明,自我批评会降低积极性和自控力,不仅耗尽了“我要做”的力量,还会耗尽“我想要”的力量。相反,自我同情则会提升积极性和自控力。
现在即未来
未来的自己会跟现在的自己不同,这是很多人都会抱有的梦想。而一旦年华逝去而自己却没有改变时,失望也就不期而至了。这样想想“中年危机”也就可以理解了,中年的自己跟老年的自己不会有任何不同,这是怎样的一种情况呢?
当然凡事也有正面的一面,一旦认识到“现在即未来”,也表明你不再需要等待虚无缥缈的未来才能实现你想实现的事情。现在的你就是未来的你,而未来的你就是现在的你。现在你做的,未来的你也将不停的重复做,而现在的你停止做的,未来的你也将一直拒绝去做,你想成为哪个你呢?
未来折现率
或者说“延迟满足感”,“未来折现率”越高的人,自然也越能为了未来的收益而在当下努力。未来的健康 * 未来折现率 是否大于 当下吃零食的满足感 在某种程度下就觉得了你下面的行为。这边是一个很有趣的实验:“Don’t eat the marshmallow!”,查看小孩对棉花糖的反应来测试他们的延迟满足感。
想像自己已经获得了自己期许的未来,而在眼前的诱惑面前,是否愿意放弃自己已经获得的期许中的未来,而交换现在的回报呢?这利用了人的“损失厌恶”的心里,失去比获得更加痛苦。
做真正能带来满足感的事情
巴浦洛夫有个非常有名的实验:摇铃后给实验的狗分发食物,经过一段时间的训练后每当狗听到铃声,就会不由自主的流口水。人类的奖励中枢也是类似,在现代社会中,我们不知不觉已经养成了很多习惯,一听到“铃声”,就会引发内心的冲动,但是冲动毕竟只是冲动,人按自己的冲动行事往往不能带来真正的满足感,事后只会感到更加的空虚。这就像是从不可能获取满足感的事物中寻求满足感。
所以作者提出要识别自己的内心冲动,它会引导我们去做能真正带来内心满足感的事情吗?比如阅读、跑步、听英语、散步、学习等。只有内心真正得到满足后人才能摆脱无处不在的冲动、焦虑。
破釜成舟
又称为“预先承诺”,限制自己的选择可能性,迫使自己只能从事有限的选项,因为如果受诱惑的自我能为所欲为的话,最终的结果只会伤及自己。
给自己未来的选择减少,提前做好准备,而对未来可能的改变预先设置好障碍,增加摩擦力,以使到时的变更更为困难。
感受冲动,但不要执行
每个人都有各种各样的想法,这是无法避免的,但是我们可以选择是否去执行。倾听内心的声音,但是拒绝行动。
十分钟冷静法
如果上面的方法你尝试之后都失败了,这也是非常常见的情况,毕竟 “Everthing worth to do is hard to do(The wisedom of the life)"。这时候给自己十分钟的间隔期,如果十分钟后你还是想做,那就去做吧,堵不如疏。
放慢呼吸法
在面对意志力抉择时,放慢呼吸 1 ~ 2 分钟,可以提高心率变异度,增加意志力储备。
结尾
这书改变了我对“自控力”很多不切实际的幻想,相比较泛滥的畅想书籍更像是一本精彩的科普书。将科学的方法应用于人的心理上而诞生的心理学,在某种程度上也具有科学那种改造自然的伟力,只不过这次的目标是我们自己。
Shidenggui
作品
归档
订阅
断舍离之公式法
16 May 2018
缘起
最近看了《自控力》,里面提到人的意志力可以分为三种力量,分别是“我不要”,“我要做”,“我想要”。作者认为意志力是类似肌肉一般的力量,可以通过科学合理的锻炼而得到增强,而我感兴趣的是里面提出的“每日实践法”,即对“我不要”和“我要做”各提出一个小要求,然后每日坚持执行来增强自己的意志力选择。针对“我要做”,我的要求是每日处理掉一件东西,而这也就是这篇文章的缘起。
在扔掉东西的过程中,会产生种种纠结,简单列举如下:
这件东西以后会不会用到?用到的时候再买会不会很麻烦,不如先留着?
花的钱买的东西就这样丢了,是不是很浪费?
这些东西承载了那么多的感情和回忆,这么丢掉真的好吗?
然后最近看到一篇博文,里面介绍了可以用公式法理清思路,比如 “天才等于百分之一的灵感加百分之九十九的汗水”,能直观的解释很多事情。因此也尝试用公式法分析下处理东西时内心的不舍到底来自哪里?
公式法
首先将相关可能的因子都罗列出来,假设如下
物品的闲置时间
物品的购买价格
当前重新购买价格
未来使用时的购买价格
投入的感情成本 :主要是针对图书而言
未来重新获得成本 : (下单时间、快递时间等),排除购买价格
未来用到闲置物品时所经过的时间 :以天为单位
物品的闲置成本 :空间成本、整理成本、心力成本
计算成本和收益
我们囤积物品是为了有朝一日能使用它,不过是出于物质目的的使用还是精神目的的回忆,则根据以上假设,如果我们未来用上这件闲置物品时获得的收益为:
未来使用时的购买价格 + 未来重新获得成本 + 投入的感情成本
而到不知何时我们能用上我们囤积的物品为止遭受的损失为
未来用到闲置物品时所经过的时间 * 物品的闲置成本
由此可以发现我们的收益是属于 O(1) 固定的,而损失却是 O(N) 随着时间增长的。而且与已经投入的 物品的购买价格 无关,这也是大家常说的 沉没成本,虽然理智上与当前的决定无关,但是在情感上却常常冒出来干扰我们的决定,留恋过去的人就是背负着这些无意义之物而活。
而且随着科技发展和生产力的发展, 未来使用时的购买价格 和 未来重新获得成本 会随着时间衰减,而我们的损失却会日积月累,更不要说 物品的闲置成本 会随着我们人生所剩时间的减少而越来越昂贵。闲置物品每存在一天,它就在消耗你的精神力、记忆力和占用你的空间,而使用时获得的 感情投入 真的那么有意义吗?
濒死体验
丢东西有时候会出现一种很奇怪的现象,比如决定了第二天要丢一个食物秤,但是真正到了第二天要丢的时候却突然下不去手,过去种种使用的记忆涌上心头,感觉像幻灯片一样闪过,人一下子就呆住了,经不住问自己,真的要把凝聚了自己的记忆和情感的东西就这样丢掉吗?往往最后又犹豫了,换了其他东西丢。
很多东西只是简单的外物,而且跟我们纠缠的也不深在舍弃时就有了那么深的留恋之心,也怪不得人们常常谈到濒死体验,即人死前一生的回忆都会涌上心头。因为我们的身躯陪伴了我们太久太久了,凝聚了我们多少的记忆、情感,在不得不舍弃时,内心的眷恋不舍又会达到什么地步?
吾所以有患者,为吾有身。及吾无身,吾有何患。 –《老子》
工具论
我在丢东西时还发现一个奇怪的现象,丢旧手机我毫不犹疑,但是丢一个家庭秤,我却好久都没有下定决心。就价格来说,旧的手机买的时候要上千,一个秤不过五十元。为什么在丢有些东西的时候会有如此的区别呢?
想了想应该是人已经习惯于借助工具扩展自我。现代人类史就是一部工具史,原始人能利用的不过是四肢五官,借此却可以成为地球霸主,更不要说现代人所使用的手机、微波炉、空调等等东西。一旦要剥夺这种能力就相当于压缩了自我的能力范围自然会引起内心的反抗。
旧手机在我有新手机的情况下意义不大,它的功能完全可以被新手机替代。但是家庭秤就不一样了,虽然它的价格便宜,但是一旦我丢了它,我就丧失了称量物品重量的能力。人作为损失厌恶的动物,由此也可以解释内心那股莫名的拒绝感的来源。
如果从另一个方向理解,把人类社会视为一个整体,我们并不是丢了工具就减弱了自我的能力,因为社会作为一个我们生存其中的集体,总是可以以合适的代价提供我所需的事物,我并不需要将所有东西掌握在自己手中,毕竟个人的能力是有限的,而适度委托也是为了让自己可以更加专注的做自己想做的事情。
我是想生活在一个自己背负着无穷的杂物而活,还是像拥有机器猫的空间口袋一般,只是需要适当的媒介就可以拥有自己想用的东西,用完即丢,轻装上阵?
结尾
人是面向未来而活,还是背负着过去踽踽而行?每天丢东西只是一件很小的事情,但有时候会引发人对自我意识的思考。我们的存在有多少是依托于外物?如果一切外物都离我而去,我又是如何成为我呢?
Shidenggui
作品
归档
订阅
读《师从天才:一个科学王朝的崛起》
27 April 2018
缘起
在公司书架上随处翻阅,偶然看到一本书带着熟悉的装帧,封面是《雅典学院》,抽出来一看发现是属于《世纪人文系列》的一本小书。这套书是上海世纪出版社出版,我以前看过其中一本,整套书的质量非常不错。我手里的这本是《师从天才:一个科学王朝的崛起》,英文名是《APPRENTICE TO GENIUS: The Making of a Scientific Dynasty》,隶属于其中的“科学人文”系列。
随手翻了下,发现是介绍几个科学家生平的传记。内容挺有意思的,就花了几天看完了。发现确实是一本很不错的书,介绍了人类如何以理性为舵、激情为帆来探索广袤的知识海洋、深入展现了科学研究中理性、人性相交织的图景,而在作者的叙述中,读者仿佛也身临其境,心潮澎湃。
概览
整本书主要围绕五个人按时间顺序讲述,分别是 美国国立卫生研究院(简称NIH)首任院长香农、香农召集的第一批部下中的药理学家布罗迪、曾作为布罗迪手下技师的阿克塞尔罗德、阿克塞尔罗德的弟子斯奈德以及斯奈德的弟子珀特,时间跨度从20世纪40年代初到80年代。其中一人获诺贝尔奖、三人获美国最高医学奖拉斯克奖。书中着重描写了布罗迪和阿克塞尔罗德的学术生涯以及他们之间的合作、冲突,还有之后阿克塞尔罗德跟斯奈德的师生情谊。
随着作者细腻的笔触,我们仿佛也回到了二战初期,看到了科学家们为了解决士兵中流行的疟疾苦思冥想,而随着战争结束他们又把目光投向了新兴的神经药理学领域,不断探索人类的知识边界。这既是一本科学家的群像史,也是美国医学逐渐崛起立于世界之巅的发展史。
下面主要谈谈书里面我感觉比较有意思的几个问题。
正文
书中的药理科学家到底是怎么工作的,他们在研究什么,又是怎么研究的?
书中的科学家,跟现实中一样也有各种工作风格,书中以布罗迪、阿克塞尔罗德最为典型。布罗迪属于工作狂类型的人,不光在工作时间激情澎湃,下班后也照样在家继续工作,同时经常拉手下与他一起通宵达旦研究讨论。而阿克塞尔罗德属于严格区分工作生活,下班后基本就不再继续相关研究,同时注重私生活隐私。
他们日常的工作主要是承接各种相关课题或者对自己感兴趣的方向进行探索。比如书中前期布罗迪和阿克塞尔罗德就是为了解决美国二战时的疟疾问题而奋斗,而战争后他们基本都是研究神经领域的一些未解之谜。在他们的研究过程中,他们手下的博士生、技师起着非常重要的作用,通常都是导师确定研究某个方向,然后让相关的博士生继续深入研究,然后他们互相讨论推进,最后评判结果看是否需要继续投入或者放弃。书中的所提的师承链就是由此而来,每个学徒都是在导师的实验室里学习,因此受到导师精神、人格的影响,代代相传。
在决定具体的研究课题之后,就进入了真正的日常工作,书中人物都是神经药理学家,更加偏向实验。你可以看到他们每天在提出理论、设计实验、进行试验并收集数据中度过。其中以进行试验最为漫长,因为牵扯到很多细节的东西。由此最常接触的事物就是试管,书中常常描写道他们工作最重要的事之一就是清洗试管,这就像是他们手中的屠龙宝刀,尤为珍重。
他们的生活跟常人有什么区别?
总览全书,科学家的生活跟常人相比可能牵涉到更高强度的智力劳动,也由此得以享受智慧的果实。交际圈基本都是行业内的人,生活比较简单纯粹。
他们的研究如何影响我们的现实生活?
里面谈到了几项研究,可以一窥科学如何作用于现实。
首先是疟疾的研究,科学家在二战过程中通过优化已有药物的给药方式,极大的减轻了疟疾的影响,保证了美军的战斗力,而在二战后期发现的新药也对抗击疟疾做出了不少的贡献。
阿克塞尔罗德发现了新的神经递质,促进了人们对精神分裂症、抑郁症的生物学理解。
斯奈德跟他的学生珀特发现了阿片受体,促进了人们对毒品上瘾机制的理解。受体的概念在我后来读《自控力》的时候也被作者用来解释人大脑的奖励系统。
科学在现实中到底以什么样的方式实践?
现有理论无法解答的问题的出现导致对应推测理论的诞生。而伴随理论而生的是可以通过试验验证的预测。通过设计精巧实验来验证理论的预测以证实或证伪,由此循环往复,不断导致新理论的推翻和建立,由此人也能从更科学的角度看待现实。
以书中药理学家而言,具体的实验过程往往让人感到不适。因为药物和神经、生理学相关的内容都需要在生物体上确认,所以可以经常看到书中的科学家磨碎猫脑、鼠脑、各种内脏之类的内容。
科学家跟他们的门徒之间的关系如何?
有好有坏。布罗迪和曾作为他手下的阿克塞尔罗德相处十几年却到最后决裂不再往来。斯奈德一直非常尊崇阿克塞尔罗德。珀特怀疑斯奈德霸占她的科研成果,发文引发学界争论。所谓有人的地方就有江湖,人性的复杂在科研领域也不例外。
二战时美国如何解决严重影响士兵的疟疾?
举全国之力,集中当时最优秀的药理学家,提供全世界收集过来的各种天然、人工制物。有了医生、测试的药物之后还欠缺实验的病人,美国就发动全国监狱的囚犯自愿参与疟疾感染试药试验,至于是真的自愿还是威逼利诱就不得而知了。由此可见美国人还是以实用为主,很懂得变通的。
书中每个人的人生图景是怎么样的?
布罗迪,博士毕业后一直从事药理学研究,喜欢通宵达旦的工作,最后获美国医学界最高奖拉斯克奖,后期因为身体原因提早退休,最后死于心脏病,享年 82 (1907-1989)。
朱利叶斯·阿克塞尔罗德,普通本科,大学做实验意外不小心失去了一只眼睛。本科毕业后在一家普通的工业实验室当操作员,10 多年后 35 岁时被征调到布罗迪手下做技师从而接触科研领域。50岁考取博士后在 NIH 单独一人继续研究,随着发展慢慢扩招,后获拉斯克奖和 1970 年诺贝尔奖,他的弟子被亲切的称为“朱利叶斯的孩子”。享年 92 (1912-2004)。
斯奈德,天才科学家,指导研究生珀特发现阿片受体,由此获拉斯克奖。而同为合作者的珀特却落选,引发不满造成诸多争端,因此错失诺贝尔奖。1938年生。
珀特,研究生时在斯奈德指导下发现阿片受体,因为没有一同获拉斯克奖之故,指责导师从中作梗,引发争议。后逐渐退出科学界进入商业领域。享年 67 (1946–2013)。
美国医学为什么会逐渐崛起立于世界之巅?
良好的社会环境,美国本土基本没有受过外来战争的影响,书中人物基本上都是在和平的环境中度过一生,能顺利的追求其理想,实现其抱负,而其他国家则相对多灾多难。美国经济的发达,二战后美国作为世界第一强国,有足够的资源和耐心投入到长期的基础医学研究中。
尾声
本书内容极为丰富,读完后我深深为书中的人物的激情、坚毅、灵性所感染,心灵也随同他们的命运起伏和时间流转时而欢欣鼓舞,时而激情澎湃,时而黯然神伤。仿佛不是在读书,而是变成了书中的一个人物,与他们同呼吸共命运。人类充分发挥他们的智力、激情去探索知识的海洋是怎样的一番图景!
Shidenggui
作品
归档
订阅
读《软件测试的艺术》
28 March 2018
缘起
看完了《富足》之后继续在公司的书架上淘书,偶然看到了一本薄薄的小书《软件测试的艺术》,英文名是 《The Art of Software Testing》。本书出版于 1979 年,距今已经快 40 年了,我手里拿的是第三版。粗粗翻了下,虽然书很久远,但还是有一些挺有意思的观点,所以花了几天时间把它读完了。
我一开始编程完全不知道有测试的概念,后面了解到 TDD,在项目中实践了下,但是有感于测试用例编写的繁琐,最后又废弛了。后面慢慢的又开始写测试了,因为高效的编程离不开测试。测试可以解放我们的大脑,专注于面向接口编程而不需要记住所有接口的内部逻辑,更不要说随之而来的减少 Bug,方便重构等好处了。
概览
整体书主要内容分为二部分。首先是软件测试的心理学,如果你不是从心底认为测试是一个好东西,又怎么能真正重视测试呢。然后是测试的方法论,讲解如何高效的编写测试用例。
正文
因为对测试我本身就怀有很多的疑惑,所以这里以问答的形式总结本书的内容和给我已启发的地方
什么是测试
我:为了保证我们编写的代码正常运行而实行的校验措施,提高我对程序能实现预期功能并在生产环境正常运行的信心。
作者:软件测试是为了发现程序错误而执行程序的过程,是为了增强软件的可靠性。
评论:我以前对测试的态度是消极的,对我编写的代码块来说,我一般认为它是能正常运行的,或者说它大概率能实现我的所思所想,而我写测试只是为了对这个大概率正常的代码进行进一步的验证,可能我多花了一半的时间,但是只剔除了小概率会失败的情况。而作者正好相反,他预期我们的代码大概率蕴含错误,而我们多花一半的时间,剔除了大概率会失败的情况,由此测试的价值大大增加。自然我们内心也有了更强的动力去写测试,毕竟谁不喜欢投入低,产出高的事情呢?
以简单的数学论的话:
假设正常完成的代码块的价值为 1,我投入 10 个单位时间完成功能代码的编写,如果有 90% 的概率能正常运行,此时代码价值为 10 * 0.9 = 9,编码单位时间价值为 0.9 / 10 = 0.09,此时投入 5 个单位时间编写测试,确保剩下的 0.1 个单位的软件价值,则测试单位时间的价值为 0.1 / 5 = 0.02,如果投入 2 个单位时间进行测试,则价值为 0.1 / 2 = 0.05,编程效率分别是之前的 22.22% 、 55.55%。我想任何一个对效率有所的追求的人都很难满意,更不要说跟创造带来的愉悦感相比,测试的编写是个相对无聊的过程。
而作者假设投入 10 个单位时间完成编写过程后,编写的代码只有 50 % 的概率能正常运行,此时代码价值为 10 * 0.5 = 5,单位时间价值为 0.5 / 10 = 0.05。如果我投入 5 个单位时间编写测试,确保了剩下的 0.5 个单位的软件价值,则编写测试单位时间的价值为 0.5 / 5 = 0.1,如果投入 2 个单位时间进行测试,则测试单位时间价值为 0.5 / 2 = 0.25,编程效率为之前的 100% 、 250%。
总结成公式的话,假设 p 为先验代码正确率,Vt / Vc 为测试/编码价值之比,t / c 为测试时间/编码时间, 则公式如下
先验证代码正确率读测试代码价值的影响之对应数学公式
转化成图表如下:
先验证代码正确率读测试代码价值的影响
可见测试的代码价值随着我们对代码正确率信心的下降而指数级上升。在这个复杂的现实世界中,懒惰而又讲究效率的程序员反而会成为测试的坚定拥护者。
为什么我们要写测试
主要有三方面:
一、编写测试可以保证代码实现了我们需要的逻辑,不至于到线上才出现一些低级 Bug。
二、测试可以快速重复多次运行,节省我们每次修改完代码后花费无聊、痛苦的手动测试时间。
三、写测试的过程可以理清我们代码的逻辑,毕竟你不能对一个你自己都不了解要做什么的代码块编写测试。
作者: 软件的运行应当是可预期、稳定的,符合最小惊异原则,软件测试是实现这一目标的手段。据统计分析,每千行代码一般蕴含 1-25 个错误,而测试的目的就是为了找出相关错误。
什么是成功的测试?
我: 没有发现错误的测试就是成功的测试
作者: 发现错误的测试才是成功的测试。类比于对病人做检查,正确查出病因的检查才可称为成功或者有效,测试同样如此。
这里作者提到一个挺有意思的心理学原理,当人知道自己的目标无法达成或者自认为无法实现时,表现会非常糟糕。
为什么测试往往难以落实到实践中
我:
一、测试用例编写编写不当,很多时候对的逻辑可能只有一条,但是错的逻辑有千千条,一开始投入编写测试用例的时间会降低增加软件开发的效率,而无法高效的编写测试用例也会导致对测试心生厌烦。
二、一开始没考虑到模块化、测试的需求,导致后期无法高效的 mock 数据,测试成本过高。
三、测试没有集成到开发流程中,导致有时候测试被破坏没有修复,根据破窗效应,后面的废弛也是预料之中了。
作者:在复杂的现实中,对软件进行完全的测试貌似是不可能的任务,但是我们可以通过相关的方法和技巧提高测试的效率,以达到可以享有测试的大部分好处而不至于投入过多的时间。
而如何高效的测试也是我最有兴趣把本书看完的动力
测试的方法论
测试的几个原则
以下是我感觉比较有意思的
通过有限的测试用例,尽可能多的发现错误:如何更有技巧的编写测试用例
测试需要多次重用:持久化测试用例的价值
程序某部分已发现的错误跟其未发现的错误成正比:如何分配测试用例的精细度
需要对无效输入进行测试:防御性编程
白盒测试
条件/判定覆盖
书里面提到的覆盖条件计算比较复杂,可以用《代码大全》里面提到的简化版来决定测试用例数
默认测试用例为 1
遇到 if / else / for / while 等条件语句,测试用例数加 1
黑盒测试
等价类划分
尽量将输入范围划分为几个等价类,使得对某个等价类元素的测试等同于对整个集合的测试。同时区分有效等价类和无效等价类。如果可能的话对输出范围也进行等价类划分。
边界值测试
测试用例的选取尽量选择边界值。边界值一般为上边界、上边界 + 1、下边界、下边界 - 1
错误猜测
尽量选取一些容易导致出错的特殊值,比如 0、None、空数组等等。这个更加依赖于程序员的经验和对编写代码的理解。
因果图分析
这个方法我没看懂,有理解的欢迎在下面回复。具体好像是利用了一些数理逻辑的方式进行了非常复杂的逻辑推断最后得出如何编写对应的测试用例。
测试的编写
以下面这个小函数为例
def double_if_less_than_ten(num):
if num < 10:
return num * 2
return num
条件/判定覆盖:默认为 1,遇到一个 if 加 1,最后需要至少 2 个测试用例
等价类划分:有效等价类为 (num < 10) 以及 (num >= 10),无效等价类为 (num 不为数字)
边界值测试: 上边界相关临界值可取 9, 10
错误猜测: num 可取 0
综合以上的条件,便可选取尽量少的测试用例覆盖尽量大的测试范围。
num expected_value 满足条件
9 18 if 为 True、(num < 10)、边界值为 9
10 20 if 为 False、(num >= 10)、边界值为 10
0 0 错误猜测 0
None raise TypeError 无效等价类 (num 不为数字)
尾声
《代码大全》里面提到,越好的程序员越是谦虚,因为他们明白编程是为了弥补人有限的智力,因此愿意通过学习来弥补。而编程糟糕的程序员往往自负,为此失去了提升自我的机会。对测试而言,我觉得下面这句话总结的很好,以此自勉。
Form is liberating
Shidenggui
作品
归档
订阅
读《富足》
7 March 2018
缘起
前几天在公司书架上看到了《富足》这本书,英文名是 《ABUNDANCE: The Future Is Better Than You Think》。大概翻了下,主要是介绍一些可能改变人类未来的技术,感觉挺有意思的就抽了几天时间看完了。
概览
整体书主要内容分为四部分。首先提出人类正在大步迈向富足,而我们时常悲观是源于远古形成的心理机制不适应现代社会所致。然后提出富足的三层金字塔概念,底层是水、食物,中间为能源、教育、信息流通,顶层则为健康和自由。第三部分是简析为了满足金字塔各个方面的需求,可以依赖哪些指数级发展的技术以及他们当前的发展情况。最后是介绍当前推动这些技术发展的中坚力量,包括底层的大多数、DIY爱好者、科技慈善家、大奖赛等等。
书出版于2012年,里面大部分都是一些介绍性的内容和数据罗列。我感觉比较有意思的是书里面提到的一些技术、项目在 6 年后的 2018 年发展的怎么样了,以及一些比较有意思的概念。
正文
100% 使用可再生能源的马达斯尔城
书中开篇以铝的价格变化作为技术改变资源稀缺的例子。在电解法制铝发明之前的中世纪,铝作为一种极轻的金属,享有比黄金还高贵的地位,但是在电解铝发明之后,廉价的铝随处可见。
铝因为电解法的发明而摆脱了资源稀缺的局面,那人类现在极度依赖的石油呢?作者认为马斯达尔城就是为了摆脱石油依赖而做出的一次尝试。这座城市投资200亿美元,于2008年开始建造,计划容纳4.5万人,使用 100% 的可再生能源。书中作者报以极其乐观的展望。那六年后的今天,马斯达尔城发展的怎么样了?
首先查看 wiki 的 Masdar City 条目,可知马斯达尔城的建成本来预定于2015年,但是目前仅建成5%的面积,预定完成时间已经推迟到2030年左右,目前可居住人口主要由一部分雇员和免费提供住宿和奖学金的马斯达尔理工学院的学生组成,大概2000人左右。
再通过 Google News 搜索相关的新闻,可以看到好几条质疑马斯达尔城已经沦为鬼城(green ghost town) 的报道。
马斯达尔城虽然暂时失败了,但是其他技术还在变革着石油领域。美国的页岩油革命越演越烈,据预估2018年美国石油日产量会超过1000万桶,超过1970的历史高点。石油价格很难再回到以前150美元的高位了。科技发展的美妙之处就在于你永远不知道是什么将会突然出现颠覆你的世界。
用邓巴数解释人为什么会对电视上瘾
邓巴数是心理学家邓巴发现的一个数字,指一个人能维持紧密人际关系的人数,约为150人。
在原始部落中,每个人的人际关系都很重要,因此大脑逐渐进化出维护人际关系网络的能力。现代科技让世界变成了地球村,但是大家反而更孤独了,很少人在现实生活中能有150个关系还不错的朋友,这样的话什么东西会填充遗留的感情空隙呢,毕竟我们大脑进化出来的人际网络还嗷嗷待哺?作者认为是电视明星。由此衍生的追星一族的狂热崇拜也就可以理解了,虽然他们只是在电视上看到他们,但是已经不知不觉让明星在心中占了一个很重要的位置。
邓巴数的可怕之处在于它是潜意识、强制性的,只要别人一直在你眼前晃而又不招致你的反感,他可能慢慢就被你内化的大脑机制当成了你亲密的朋友之一。这跟心理学中的多看效应有异曲同工之妙,被广泛应用于现代广告之中。
书中只谈了明星,但是回想现实中的各种大V、直播的流行,是不是反应着日渐孤独的现代人需要填补的感情空隙越来越多了?
难道要坐等我们的大脑被现代科技所填满吗?这让我想起以前尼采曾说: “在被战胜的激情的土地上播下优秀的精神著作的种子,是这时候最直接的紧迫任务。…… 如果对它不是这样来看待,那么各种各样的杂草和可怕的东西就会迅速在这片变得空旷的肥沃土地上长出来,很快它就变得比以前任何时候都更加丰满、更加狂野不羁。“
如何衡量人类的技术进步
现在技术的飞速发展毋庸置疑,智能手机、vr、人工智能、可回收火箭,层出不穷、眼花缭乱,有时不由感慨人类创造的才能。谈到技术进步,常常会听到汽车、高铁、飞机在速度上的提升,摩尔定律带来的信息革命,有没有一个统一的标准可以用来衡量历史发展中人类进步的程度呢?
书中提出了基于特定历史时间段,人类获取某样东西所花费的平均时间来衡量技术上的发展,因为时间对所有人都是公平的。
真正能够衡量一个东西价值的,是想得到它你所必需花费的时间。
获得一小时的人工照明:公元前1750年,使用芝麻油的话需工作超过 50 小时; 19 世纪 80 年代,使用蜡烛的话需要工作 6 个多小时,使用煤油灯,需要工作 15 分钟;现在,你只需工作不到半秒钟,不到公元前 1750 年的 35 万分之一,而且更加方便、安全。
从波士顿旅行到芝加哥:19世纪,乘坐公共马车需要两个星期时间加普通人一个月工资,现在飞机只需要两个小时以及一天的工资就足够了,综合成本只有以前的五千分之一。
通过相同的工作时间,我们比以前的人获得更多的水资源、卡路里、流明、里程、能源、信息,更不要说许多前所未有的新事物了。1000年前的人如果想上网一小时的话,他们要付出多少工作时间,而古时的君王又能支配多少劳力?从富足来讲,古时的君王不如现世的平民,而当下的巨富不如未来的贫民也是可以预期的了。
Pale Blue Dot
1977 年,Voyager 1 从地球出发,探索茫茫宇宙。1990 年,应天文学家和作家 Carl Sagan 之请,从 60 亿公里外拍摄了这张著名的照片。Carl Sagan 对此有一番精彩绝伦的演说。
如果真的有天外来客的话,会不会嘲笑人类在这小小的地球上,还有那么多恩怨纠葛、战争杀戮。
and all were not worth a passing glance — “SIDDHARTHA”
基因工程
1953年人类已经知道了生命密码 DNA 的组成,但是现在人类离自由操纵 DNA 还很远。2011 年左右我在环球科学上读到了人工生命合成成功的文章,科学家希望借此编码出能合成类石油化合物的藻类来解决能源问题,可惜此后就很少看到类似的进展。
直到最近在《富足》里又看到了相关的活动发展,里面介绍了《国际基因工程机器大赛》这项赛事。参加者利用 DNA 创造各种功能的生物积木来实现特定功能,并贡献到公开的数据库中。如果把基因工程比成编程的话,幸运的是编程语言只有一种,就是 DNA,但是不幸的是没有语法说明书,只能靠大家去摸索各种 DNA 序列的意义。
而且随着 2010 年后 CRISPR 技术的发明,这项简单、高效、便宜的技术也将由大型机构垄断的基因编辑技术开始普及到个人手中。已经有人尝试编辑自己的手臂相关部分的基因,使其肌肉变得更为发达,而相关工具的费用低于 $400 的。他们自称为 biohacker ,真像 20 世纪 80 年代,随着大型计算机小型化而来的 hacker 文化,我们是不是在黄金时代到来前的黎明呢?
人造肉
肉作为人类蛋白质主要来源之一,有着非常重要的地位,但是自然养殖肉类需要耗费大量的水、土地、食物、时间,而且后续的屠杀、处理带来很多污染因素,总体效能低下。随着科技的发展,自然会想到直接养殖肉类细胞,让它们直接无限复制,由此得来的便是人造肉,英文称 clean meat。
谈到无限复制的细胞,我第一印象想到的就是因为突变失去控制的癌细胞,只是这里变成了科学家人工干预,但是由此而来的心理不适还是存在了一段时间。人造肉的效率毋庸置疑,只是因为技术限制,导致商业化一直受限,在作者所在的 2012 年还只是一种纸上谈兵。但是 6 年后的今天,关于人造肉的最新新闻是《美国牛肉协会要求区分人造肉和真实牛肉,人造肉不能称之为‘肉’》,而美国一些餐厅也已经开始销售人造肉汉堡。
随着人造肉技术的成熟,以其高效的能源利用率带来的低廉价格,占领市场也是预料之中的事,看来以后在“有机蔬菜”、“有机水果”之外又要多加一个“有机肉”了。
BoP business(Bussines of the Bottom of the Pyramid)
作者把此章冠名为《崛起的十亿人》,世界上每天生活费用不足 2 美元的人群达到 40 亿人之巨,以前人们认为在他们当中并没有什么机会存在,但是随着科技的发展,为底层的十亿人服务并实现双赢成了可能。
以智能手机为载体的科技浪潮极大的降低了人们获取服务的成本,只需要一部手机加电信网络,就可以加入全球互联网。而有志于此的服务商也只需要将服务构建在互联网的载体上,便对底层的 40 亿人触手可及。
Grameenphone 便是例证之一,这家创建于 1997 年的公司致力于在孟加拉国开展面向农民的电信小额贷款业务,至 2011 年 2 月用户数达 3000 万,同时在发展过程中又陆续投入 16 亿美元建设电信网络,不仅本身取得不菲的盈利,而且降低了全国的贫困率。据 wiki 的数据,截止 2016 年 8 月它的用户数达 5450 万。
而国内也有瞄准这一市场的公司。另类的非洲之王传音手机:出口8000万部 国内零销量,便是 BoP businnes 的绝好例证。
Adjacent Possible
作者在书中提到一个很有意思的观念,叫“相邻可能”。技术的发展使人们犹如置身于一个有很多门的房间,随着人们打开一扇扇门,房间随之扩大,而越来越多的门也得以出现,指数级增长的门和几近无限的房间将带领人类走向无限的可能,而我们现在就处于这一梦幻般的历史进程之中。
The adjacent possible is a kind of shadow future, hovering on the edges of the present state of things, a map of all the ways in which the present can reinvent itself. As Steven Weinberg notes, the adjacent possible “captures both the limits and the creative potential of change and innovation.” — Eddie Smith (2010)
随着轮子的发明,手推车、自行车、汽车、溜冰鞋随着成为可能。随着晶体管的发明,电脑、智能手机、互联网成为可能。
自然界的所有蛋白质都由 20 种氨基酸组成,而如果 20 种氨基酸组成长度为 100 的蛋白质,有 20^100 可能,相比之下全宇宙的原子数约为 10^80。地球上所有现存的蛋白质只探索了可能存在的蛋白质样本空间的极小一部分就创造了我们现在的一切。令人惊奇生命到底还有多少未知的可能性,不由遥想所有可能性都得以展现的未来又将是什么样的一番景色?
Life has explored only an infinitesimal fraction of the possible proteins … then there is plenty of room for human explorers to roam. — Stuart A. Kauffman, “At Home in the Universe”
而我们自己,是不是也只探索了无限可能的一小部分,而未来又有什么在等待我们呢?
尾声
《富足》一书的阅读感想现在就告一段落了,希望人类真能如书中所说走向富足的未来。
好几年没有动笔写一篇正经的文章了,本来就想谈几个主题,没想到越写越多,加上查资料的一些时间,一下子就过去了 20 多天。不过发现写作跟阅读一样,都是一件很有乐趣的事情,以后应该会坚持写作,希望能给自己、也能给读者带来一些思考。
Life is such a poor business that the strictest economy must be exercised in its good things. — Arthur Schopenhauer, “THE WISDOM OF LIFE”
全栈开发(Full-Stack)到底是什么?Google出的第一条结果说全栈开发就是需要对前后端技术都了解的人,同时可以把产品设计界面转化成代码。总之就是对开发产品涉及的领域什么都需要了解的人。
我们知道现代社会的高效是建立在合理的分工合作之上的,个体的能力有限,所以需要每个人发挥自己擅长的能力,然后合作解决一个大项目。企业的出现就是因为社会的这种需求,所以产生了上班的这个概念。这个话题讨论起来很漫长,所以让我们看看全栈开发又是为何而出现并流行起来的。可能有人不认为全栈开发很流行,这里有一份StackOverflow的2015年调查报告(Stack Overflow Developer Survey 2015):
从调查结果来看连续三年全栈开发都是位列第一的,不得不说越来越流行。当然我们为什么在中国感受不是很明显呢,看这张图就知道了,因为国人访问StackOverflow还是比较少的,而且这个网站并没有被墙。
全栈开发(Full-Stack)到底是什么?Google出的第一条结果说全栈开发就是需要对前后端技术都了解的人,同时可以把产品设计界面转化成代码。总之就是对开发产品涉及的领域什么都需要了解的人。
现代社会的高效是建立在合理的分工合作之上的,个体的能力有限,所以需要每个人发挥自己擅长的能力,然后合作解决一个大项目。
企业的出现就是因为社会的这种需求,所以产生了上班的这个概念。
全栈开发为什么越来越流行?我认为这是因为技术本身的进步带来的,技术发展已经越来越开放并且模块化了,很多产品完全可以用现有的模块去重新组合二次开发,很多关键技术和算法都已经不是什么阻碍了,越来越流行的Github和StackOverflow已经表明了这一点。连机器学习和人工智能这种高大上的技术都已经有开源模块可以学习并应用,以后这个趋势会越来越强。个体的影响力正在被不断的放大,反而企业的影响力会降低,未来的社会中远程工作和独立工作者的数量会越来越多。社会对群体聚集进行生产产品的模式需求在降低。
那这种趋势的来临对我们个体有什么影响?首先不论你工作和IT有无关系,你都会被互联网这种趋势影响的,所以有必要去了解全栈思维对我们个体的影响。首先让我来解释下我理解的全栈思维:它应该是一种解决问题的能力。让我们假想两种思维模式就可以看出这个定义的有趣之处了。A是你要打造一款产品,然后你需要了解制作这款产品需要的资源、技术和流程。B是你需要学习一门流行的技术来找工作,然后你利用这个技术来做一个Demo产品。A和B的区别在于A需要解决各领域的问题,A是一种全栈思维,以目的为驱动的。为了完善这款产品你也许需要了解产品的各个方面,这在以前对个体是不可能完成的任务,但是在这个不断被链接的世界里,每个人都变得更为专业,通过个体的创作与链接可以使你完成一项大工程,这里面甚至你都不需要组建团队,一个人就可以完成产品的创作。这在接下来的部分我会介绍我如何在三个月内完成一款产品从0到1的过程。相信看完你会对全栈思维有更深的理解。
那么全栈开发的好处和坏处有哪些呢?我认为好处是沟通成本为0,沟通在一个大工程里面是非常有影响的一个环节,很多时候项目的失败和沟通有很大关系,根本原因在于参与者太多,成本已经大于参与者协作的加成了。那坏处就是什么工作都需要你来做,当然你可以外包一些不重要的部分出去,但是一定要注意沟通成本的增加。
产品设计
我要做的产品是一款有服务器端支持的APP,目前我只做了iOS版本,未来可能要做Android和Web版本。这款APP已经上架(http://diary.trade)。它是一款工具类的产品,所以我在开发的时候使用Native模式来开发的,并没有用Web封装这种方式,因为工具类更适合用Native来开发,内容类的用Web来开发也不错。
这款产品前身是我设计的一个Excle版本的资金管理系统,所以不是很方便,这促使我开发了这款APP,因为我本身也是做二级市场交易的,所以这款产品本身就作为我的一个辅助交易的工具,可以节省我很多时间。同时这种交易理念也经受过时间的考验,所以有必要做出来让大家用,可以提高你的交易能力和风险管理能力,这比选股择时更重要。
这是规划中的版本功能设计。
项目管理
整个项目计划管理是用OmniPlan制作的。APP开发花的时间最长,因为我使用了Apple的Swift开发语言,算是一遍学习一遍来开发,所以进度要比计划的慢一些。
产品开发
系统模块设计
APP本身由服务器端来计算很多数据,和服务器通过TLS加密通道交换JSON格式数据,APP口令存储在iOS本地Keychain中,通过OAuth服务器获取Access_Token来授权认证,API的设计是REST风格。
系统模块做了如下划分:
考虑到i365系统中不需要做复杂的事务操作,所以采用MongoDB
Document间采用关联模式建立连接关系,少部分有内部关系的Document采用嵌套模式建立联系
这是用MysqlWorkbench画的ER图,但是我最终采用的是NoSql的MongoDB,所以这只是一个参考的图,这不是最终设计版本。
REST API设计
此处我在Python遇到26个坑,开发时间半个月,v1版本代码行数2K+。
UI/UX设计
此处的坑在于我没有学过设计,需要不停的画了再删再画直至满意,当然我还是遵循了一些基本的设计原则,这可能是我以前看的设计的书籍还没有忘的原因。 Sketch的教程我使用的是网上的视频教程。
服务器运维
服务器是阿里云1G单核配置,连多进程都玩不了。用iptables做了些安全设置。
OS是Ubuntu。
Web服务器是Nginx。
Python用的是UWSGI处理。
数据库是MongoDB。
日志监控是Nagios。
此处遇到22个坑。
APP开发
开发平台:Mac OSX
IDE:Xcode
开发语言:Swift2
此处遇到71个坑,开发时间两个月,v1版本代码行数7K+。
WEB开发
产品和项目展示页面:
直接搜索App Landing Bootstrap Template就可以找出很多免费的模版。
系统测试
API通过CURL来测试JSON数据。
APP是人肉测试。
以后考虑使用单元测试。
域名
万网购买的几个域名包括cn、net、tech、trade。费点时间的就是去备案了,这个万网有很详细的流程说明。
产品反馈
通过邮件、微博和微信公众号来接受用户反馈意见,APP本身没有加入崩溃检测代码,以后可以考虑加入。
寻求帮助
通过Google、Github、StackOverflow搜索来解决问题。但是如果你想对某个主题进行系统学习的话可以通过在线MOOC课程。这个以后我会分享我在Coursera里面上的十几门课程。
运营推广
产品文案
这个就需要你多看一些产品介绍文案了,然后写多个版本,然后挑选一个最好的。文案的关键是要抓眼球,也就是在最短的时间给潜在的用户介绍你自己,同时也要遵循平台本身的特点来推广,要和平台的逻辑和气质相符效果才棒。
这是我给App Store写的产品文案,考虑到App Store的搜索只能针对标题,所以标题的设计就很重要,这其实就是ASO优化的主题了。如果是Android的在Google的Play Store文案你就需要考虑内容的介绍加入更多的关键词了,因为Google会搜索它的。
运营
如果是工具类的产品对运营的需求倒不大,但如果是内容类的产品那么好的运营就很重要了,需要和用户进行互动,不断的制造话题,这方面可以多看看微博和知乎的热门话题产生的逻辑,多尝试并进行分析改善。
热门话题一般都是经过精心设计的。
推广
推广有很多种方式,有钱的话那就有很多种暴力推广方法了,比如微信公众号的推广平均一个用户都涨价至几块钱一位了,所以大家都在做吸引眼球的动作了,只要有关注者,都是钱嘛。以下是几种常见的推广渠道:
邮件列表。优质的邮件列表推广的效果很不错,所以我们可以尝试去维护一个邮件列表,定期发送用户关注的内容,比如做一个PodCast或者周刊之类的Mail List。邮件列表发送商的话我一般用Mailchimp,发送成功率和模版及统计功能都很棒,还有移动端的App。
微博。微博的确没有死,焕发出了第二春。微博的好处在于可以制造传播话题,同时和用户互动方便。
知乎。知乎需要专业的去运营,懂得某领域的知识,然后在热门话题下多分享回答就可以不断积累高质量的用户源了。
微信公众号。公众号的好处在于和用户的单向互动,有些不想让用户之间了解的内容需要运营的话很适合用公众号,因为用户之间不可见,可以避免很多问题。
社区。社区的好处在于主题的集中度很高,比如雪球、V2EX社区等,都是很专业的社区。
END OF DOCUMENT –
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################
##########################################