無標題文檔

使用 Nginx 反向代理阿里云 OSS

上次将 Blog 的主机重新整理下了以后,这次抽空将站点的静态资源也整理了下。

虽然使用又拍云有一段时间了,也非常的稳定,但毕竟要「居安思危」,又拍云同时又提供了「融合云」的第三方存储的同步方案,于是就选择了阿里云的 OSS 作为存储的备份方案。

Aliyun OSS

上图是我目前的方案,客户端这边通过脚本或者其他工具上传到又拍云上,然后又拍云自动将增量数据同步到阿里云的 OSS,最后我再使用 Nginx 反向代理去访问两块同步的资源。

为什么使用反向代理,是因为由于「众所周知」的原因,在国内绑定 HTTP(S) 服务的域名是需要备案的,我觉得麻烦同时也不想浪费个域名做这样的事情。

同时使用反向代理的方式,后面自己还可以控制缓存、负载均衡等,可操作性更大一些。长话短说,在配置阿里云 OSS 的反向代理的时候碰到了些坑,在这里记录下。

首先,在阿里云 OSS 后台务必设置好权限,在 Bucket 中设置读写权限为「公共读」,以及在 Refer 设置为自己实际的情况(很重要)。

下面是目前的针对阿里云 OSS 的 Nginx 反向代理的具体配置,供参考:

location /<base_url>/ {
    proxy_pass http://<bucket>.oss-cn-shanghai.aliyuncs.com/;

    proxy_redirect off;
    proxy_set_header Host '<bucket>.oss-cn-shanghai.aliyuncs.com';
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header User-Agent $http_user_agent;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_cache_valid 200 302 3600m;

    proxy_buffer_size 256k;
    proxy_buffers 4 256k;
    proxy_read_timeout 600s;
    proxy_send_timeout 300s;
    proxy_temp_file_write_size 256k;

    expires max;
    etag off;
}

proxy_set_headerHost 必须要设定自己对应的主机名,例如我的阿里云 OSS 对应的主机在华东2(也就是上海机房)阿里云 OSS 是根据 Host 确定 Bucket 的名称的。顺便提一句,如果 Nginx 主机也是和 OSS 同个机房,那么可以使用内部 VPC 内网节省流量。

OSS Endpoint

由于反向代理的是静态资源,所以我考虑尽可能的多缓存时间以加快速度和减少请求流量,所以设置了 proxy_cache_valid 200 302 3600m 以及 expires max 强制缓存。

然后 reload Nginx 以后就可以看到效果了。由于目前 又拍云 和 阿里云 OSS 的内容是同步的,所以后面使用 Nginx 做负载均衡也是比较方便的事情,这里就不重复贴配置了。

顺便在这里吐槽下,个人不是很推荐买阿里云的香港服务器!因为又是那个「众所周知」的原因,阿里云的香港节点 HTTPS 流量是会被部分 ISP 给污染的

这块个人也是困扰了很久,然而不打算处理了,毕竟现在写 Blog 的人不多看 Blog 的人更少,被墙了反而能够更加自在些。

- EOF -

随谈 Java 的空指针(翻译和整理)

整理自: http://www.yegor256.com/2014/05/13/why-null-is-bad.html

在日常编码中,有个经典的 Java 空指针(NULL)调用如下:

public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return null;
  }
  return new Employee(id);
}

那么这段方法有什么问题?这个方法的最大问题就是有可能会返回 NULL 对象。空指针(NULL)问题在面向对象编程角度上说是个很严重的问题,所有面对对象编程的过程中都会碰到类似的问题。甚至 Tony Hoare 向全世界道歉,忏悔他曾经发明了「空指针」这个玩意

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement.

原来,在程序语言中加入空指针设计,其实并非是经过深思熟虑的结果,而仅仅是因为它很容易实现而已。这个设计是如此的影响深远,以至于后来的编程语言都不假思索的继承了这一设计,这个范围几乎包括了目前业界所有的流行的编程语言。

对许多程序员来说,早就已经习惯了空指针的存在,就像日常生活中的空气和水一样。那么,空指针究竟有什么问题?

额外的错处处理

当然,有经验的编码人员自然会时刻意识到空指针问题的存在,那么有可能在编码的过程中加入额外的判断,就会规避这个问题,例如下面的写法:

// this is a terrible design, don't reuse
Employee employee = dept.getByName("Jeffrey");
if (employee == null) {
  System.out.println("can't find an employee");
  System.exit(-1);
} else {
  employee.transferTo(dept2);
}

那么这样的话就会多了额外的流程去处理因为空指针判断而多加的判断。例如,上面的处理逻辑和流程中,如果按照面向对象的思维书写,不考虑空指针的情况,只是简单的:

dept.getByName("Jeffrey").transferTo(dept2);

就可以。加入空指针判断以后,程序往往会有额外以及复杂的逻辑判断。

语义不清晰

上面的问题自然可以规避,例如在 getByName() 这个方法中加入异常的判断,那么重新命名这个方法的名字为 getByNameOrNullIfNotFound() 。这样子从代码可读性的角度上说,从方法名字就可以判断这个方法会返回什么,
但是自然而然为了表意清晰就会让方法的名字变得越来越长(感觉有点在黑 Objective-C)。

为了表明这个歧义,除了正常的返回值以外,其他的空指(NULL Object)针情况都会返回一个异常。那么这里又会引申出另外一个问题,就是性能。从上面的代码考虑,如果返回了 NULL 以后再抛出一个异常,实际上这段代码是已经被执行了的,然后再加的判断。

例如,在示例中 getByName() 通常是个 Map 然后执行了 get 方法去搜索是否存在这个名字,如果不存在再抛异常,这样就会造成额外的执行时间:

Employee employee = employees.get("Jeffrey");
if (employee == null) {
  throw new EmployeeNotFoundException();
}
return employee;

当然,这个问题也是很好规避,例如只需要先搜索它的索引即可,然后再取值返回:

if (!employees.containsKey("Jeffrey")) { // first search
  throw new EmployeeNotFoundException();
}
return employees.get("Jeffrey"); // second search

那么,这部分操作判断只是针对空指针就多了一次搜索操作。我们考虑再优化下,直接使用迭代器处理:

Iterator found = Map.search("Jeffrey");
if (!found.hasNext()) {
  throw new EmployeeNotFoundException();
}
return found.next();

这样子似乎很好的解决了问题。

但是,我们编写这段代码的业务初衷似乎越来越远,然后方法的名字也变得越来越长,自然而然代码也变得越来越复杂。

如何最大程度得避免空指针?

本篇开头的这个问题一样,空指针的问题可以追溯到计算法发展史时期,同时空指针异常的情况也很多,甚至在程序运行阶段也无法避免空指针的情况。那么,在编码层面,我们需要注意哪些呢?

确认调用的的每个变量都已经被初始化

这点说起来很简单,但事实上随着业务的发展项目代码也会越来越庞大。这时候方法之间调用的关系也会越来越复杂,很难避免使用到的方法都已经明确被初始化。

所以这块单独放在这里,需要我们在编码的实话重点考虑变量存在的可能性,这其实大体上基于自己的实际编码经验。

尽量使用明确的值调用

如果已经明确某个变量(常量)的值,那么是可以安全调用它的方法的。例如对比下面的几行代码:

String a = null;
a.equal("b");   // 会产生空指针异常
"b".equal(a);   // 推荐的写法

很明显使用常量去做调用这代码会更健壮一些。

尽量避免在函数中返回 NULL

当如果在编写方法中考虑返回 NULL,这个时候则需要冷静下是否真的需要这样子做。因为,通常来说会有比返回 NULL 更好的处理方式。

自动装箱需谨慎

自动装箱确实为编写程序带来很多方便,但我们在编程时候也不能滥用自动装箱。

比如,下面这个程序依然存在空指针异常隐患:

Person jack = new Person("jack");
int weight = jack.getWeight();

这种异常在我们使用一些 ORM 框架中会碰到,如果数据库对应的对象并不存在该值,而我们又在类中使用了一个基本类型与之对应,依然就会抛出空指针异常。在这种情况下就尽量使用包装类来对应,并且在使用该值时候先判断是否为空。

遍历谨防集合为空

for (int num : list) {
    // for each num in list
}

及时验证外部数据

在代码运行的过程中,尤其在解析外部数据的时候可能会引发影响不到的问题。例如下面的 Json 数据

{"name": null, age: 28}

如果不处理完善,虽然这可能是外部原因造成,但直接使用 name 属性也会导致空指针的问题。

使用第三方库加强验证

很多第三方的 Common 库都会有验证空指针的方法,例如 Guava 中针对空指针的判断有个单独的包去处理。

Optional<Integer> possible = Optional.of(5);
possible.isPresent(); // returns true
possible.get(); // returns 5

或者过滤 NULL 也会更加的方便

Joiner joiner = Joiner.on("; ").skipNulls();
return joiner.join("Harry", null, "Ron", "Hermione");

使用 @NotNull 或 @Nullable 注解

强烈建议多使用注解来增加代码的可读性,例如多增加 @NotNull 或 @Nullable 注解,也可以加强代码静态检查方面可能会造成空指针的可能性,具体可以参见这里

使用 Java8 的 Optional

很多「现代」语言都会有针对变量为空的可选链式判断,例如

Groovy 语言有一个 ?. 的操作符,可以安全地处理潜在可能的空引用(据说 Java7 曾被建议引入这个但是并没有发布)。它是这么用的:

 String version = computer?.getSoundcard()?.getUSB()?.getVersion();

虽然 Java 看起来非常的保守,但好在 Java8 中增加了 Option[T] 这个对象包来代表类型 T 的某一个值存在或者没有。那么上面的代码可以写成:

String name = computer.flatMap(Computer::getSoundcard)
                      .flatMap(Soundcard::getUSB)
                      .map(USB::getVersion)
                      .orElse("UNKNOWN");

看起来似乎有点麻烦,但相信我你会爱上这样的写法,具体可以参见这里

好了,针对空指针的总结和整理先到这里,如果你有更好的意见和建议,欢迎不吝提出。

参考资源

-- eof --

使用 Docker 部署 VPS 备忘

本 Blog 的代码也有很多的年头,当时使用的技术过了几年在目前看来有些的陈旧。同时,在 Vultr 的服务器空间也即将到期,所以考虑干脆做个迁移顺便将目前 VPS 上的服务也一起整理了下。

这一整理,发现了不少的巨坑,这在这里记录下。先说说在迁移前 VPS 目前的主要问题

  1. 部分代码老旧,同时又有新的代码,运行环境就有好几套;
  2. 尝试使用 Node 的工具进行开发,然而 Nginx 以及 php-fpm 独占了 80 端口,配置文件越来越复杂;
  3. 权限分配不合理,有部分的权限使用 root 运行有安全上的风险;
  4. 备份机制不合理,没有做到企业级的多点备份(虽然个人 VPS 没必要,但图个心安)。

其他情况不多说,重新整理以后的 VPS 应用架构图如下,相对来说还是不是很复杂:

https://friable.rocks/_/2017_10_12/1507794332@800.png

综合价格以及习惯的因素(可能是情怀更多?),大部分选择了阿里云的服务。同时针对以上的问题,这边的解决方案是:

  1. 使用不同的 Docker 集群将 php5 和 php7 的应用分别调用,相互不受影响。老旧的代码就让它们在 php5 环境中「颐养天年」;
  2. 最前端使用 Tengine 分发请求到每个对应的 Docker,方便管理;
  3. 使用 Docker 以后,权限这块就相对来说好控制很多,宿主机不受任何的影响;
  4. 配置文件使用 git 管理起来,并托管到 Bitbucket,方便部署;
  5. 备份使用阿里云的 OSS 存档服务,价格很便宜而且也是安心很多。

同时,在这里汇总下碰到的几个坑和心得,供参考

  1. 官方 Docker 的 php7-fpm 是没有带 gd 库的,所以要自己 build 一个版本(生产环境顺便把 debug 这些都去掉);
  2. 因为只是在最外面 Router 应用服务使用 https,所以 PHP 应用内部需要增加判断下,防止判断失败,详见这里。简单的说,就是加个判断
/**
 *  Handle SSL reverse proxy
 *  @see https://www.variantweb.net/blog/wordpress-behind-an-nginx-ssl-reverse-proxy/
 */
if ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
    $_SERVER['HTTPS']='on';
}

if (isset($_SERVER['HTTP_X_FORWARDED_HOST'])) {
    $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST'];
}
  1. Alpine Linux 的 apk 源有时候会不稳定,Docker build 的时候会有出错的几率,多重试就好。

--

补充下,有关 HTTPS 转发这块,khsing 提出了很好的建议,非常感谢:

刚才看了你的使用 Docker 那篇文,里面处理 WP 处理 https protocol
的方法还挺硬的,这事儿我之前在阿里云部署的适合也遇到了,不过我是在 nginx 上做了处理,这样就不用动 PHP 代码了。

map $http_x_forwarded_proto $forwarded_https { default off; https on; }
fastcgi_param HTTPS $forwarded_https;

-- eof --

我的照片

嗨!我叫「明城」,八零后、码农、宁波佬,现居杭州。除了这里,同时也欢迎您关注我的 GitHubTwitterInstagram 等。

这个 Blog 原先的名字叫 Gracecode.com 、现在叫 「無標題文檔」 。 要知道作为码农取名是件很难的事情,所以不想在取名这事情上太费心思。

作为八零后,自认为还仅存点点可能不怎么被理解的幽默感,以及对平淡生活的追求和向往。 为了避免不必要的麻烦,声明本站所输出的内容以及观点仅代表个人,不代表自己所服务公司或组织的任何立场。

如果您想联系我,可以发我邮件 `echo bWluZ2NoZW5nQG91dGxvb2suY29tCg== | base64 -d`

分类

搜索

文章