不恰当使用线程池处理 MQ 消息引起的故障

现状

业务部门反应网站访问特别慢,负责运维监控的同事说MQ消息队列积压了,中间件的说应用服务器内存占用很高,GC 一直回收不了内存,GC 线程占了近 100% 的 CPU,其他的基本上都在等待,数据库很正常,完全没压力。没啥办法,线程、堆 dump 出来后,重启吧,然后应用又正常了。

分析

这种故障之前其实也碰到过了,分析了当时 dump 出来的堆后发现,处理 MQ 消息的线程池的队列长度达百万级别,占用了超过 1.3G 内存,这些内存都是没法回收的。

程序的实现目前是这样的:关联系统把消息推送到 MQ 上,我们再从 MQ 上拉消息下来处理;每种类型的消息都有一个线程负责从 MQ 上拉消息,拉下来后封装成线程池的任务提交给相应的线程池去执行。代码可以简化为:

package net.coderbee.mq.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MQListener {
     public ExecutorService executor = Executors.newFixedThreadPool(8);

     public void onMessage(final Object message) {
          executor.execute(new Runnable() {
               @Override
               public void run() {
                    // 耗时且复杂的消息处理逻辑
                    complicateHanlde(message);
               }
          });
     }

     private void complicateHanlde(Object message) {
     }
}

继续阅读

踩坑之 双机热备切换

这个问题还是去年在上家公司时碰到的,当时领导要我优化一个后台系统,它的核心逻辑就是接收客户端上传文件,把解析出的信息入库,再把文件上传到一个 FTP 服务器。

搞完之后总得部署到生产环境,因为这是个接手系统,只有源码,也不知道部署结构,只告诉我有两个结点,部署上去之后验证下没问题就行了。

我当时是准备一个一个结点部署的,在 serverA 上更新完成,验证后发现没有问题,然后准备更新 serverB 时,却发现突然又不行了,当时好像是请求没收到还是咋,具体不记得了,只有一个人在那里,折腾很久,最后联系运维人员,人家把双机热备的切换机制给停了才行。

事后分析认为是双机热备切换导致的,主机更新后一开始正常是因为还没有切换到备机上,而主机上的服务很快就起来了,请求仍然是到了主机上,所以一开始验证是可以的,但后来被切换到备机上去了,备机上跑的是旧的程序,请求不会到主机上,在主机上看是怎么也不会知道原因的,还以为程序有问题,当时也不会抓包分析,人都慌了,哈哈。

现在的 web 系统为了达到高可用,都会进行双机热备,就是两台服务器 serverA 和 serverB 上的部署是完全一样的,它们都配置了同一个虚拟 IP,但只有一台机器对外提供服务。如果这台提供服务的机器出故障了,双机热备程序就会自动把请求切换到另一台服务器上,从而保证服务是可用的。但这个切换是有延迟的,取决于双机热备的实现机制和配置。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

故障之 CDN 使用问题

故障问题

最近公司的一个系统用上了 CDN 服务,用户反应快多了,那个系统主要是静态资源展示和一些可公开的数据查询服务。另一个项目组的系统A看了也要上,不过这个A的后台代码是放在我所在项目组M里,是同一个应用,都在同一个域名 main.com 下。网络组把 main.com 的 DNS 解析到 CDN 服务器上去之后,访问也是快了,但是用户反应数据乱了,而且有些服务没法用了,后台统计特定地区用户的访问也不准了,因为所有请求基本上都是从几个 IP 里过来的。

问题原因

稍微对 CDN 有所了解的人看完问题描述之后都会发现问题所在:由于只有一个域名 main.com ,且被解析到 CDN 服务器上,CDN 不仅分发静态资源,还变成代理服务器了,所有动态请求、数据都要经过 CDN 服务器。

很明显,这是错误使用 CDN 带来的问题。这带来的问题不仅是上面描述的问题,更大的业务安全问题:当所有业务数据都经过 CDN 服务器时,CDN 厂商就有了那些业务数据,这相当于把银行卡和密码交给一个中间人,要存钱、取钱的时候就让这个中间人去 ATM/银行 处理,再给自己反馈结果。有这么值得信赖的中间人吗??

继续阅读

Akka TypedActor

Akka 中的有类型 Actor 是 Active Objects 模式的一种实现,将异步的调用执行逻辑封装在一个方法内,在代码层面保证了的顺序执行思维。

Active Objects 设计模式

来自维基百科 Active Objects

该设计模式包含了六种元素:

  • 代理:提供了面向客户端的带有公开方法的接口。
  • 接口:定义了到 active object 的请求方法(业务代码提供)。
  • 来自客户端的一序列等待请求。
  • 调度器:决定接下来执行哪个请求。
  • active object 方法的实现类(业务代码提供)。
  • 一个回调或变量,用以让客户端接收结果。

上述六个元素中,除了标记(业务代码提供)的,其余都是由该模式的实现提供的,在本篇也就是 Akka。

Akka 是通过 JDK 的 java.lang.reflect.Proxy 来自实现 active object 模式的。

继续阅读

我眼中的 性能劣化、优化的曲线

性能劣化

性能劣化的图形

随着代码写得越来越烂,程序运行时 数据库操作更多、IO 阻塞等待跟过、不必要的对象创建、GC 回收更频繁,线程的上下文切换也更多,开销越来越多,所有因素综合起来,程序运行更慢,响应延迟加大。

当到达临界点的时候,压垮骆驼的最后一根稻草出现了,系统直接崩溃。

坏东西、副作用是会累积的。

继续阅读

《Effective Ecterprise Java》 笔记

只有第7章的。

关于安全:它是站在屋子中心的大象,每个人都可以看到它,每个人都认识它,但每个人都拼命地试图对它视而不见,绕开它进行工作。

  • 认证(authentication):是一种校验行为,检查系统中的一个实体是否真的与它声明的身份相符。认证有三种基本形式:根据你所知的、你所有的或你是什么来进行认证。
  • 授权(Authorize):授权行为是确认系统中的一个实体能够做什么。很多时候,成功的认证导致某种授权。

安全是一个过程,而不是产品

如果你以为技术可以解决你的安全问题,那你根本没有理解这个问题,并且你也没有理解这项技术。

安全不是那种我们可以简单地在系统实现的生命周期的某个时刻 “打开” 的特性。安全必须贯穿于系统开发的每次迭代过程的分析、设计、实现和测试中,否则,漏洞就会出现。

“编写安全代码” 应该是程序员的一条指导原则,就像 “编写好的代码”、“编写优雅的代码” 那样。

安全不仅仅是预防

任何安全系统的另外两个部分就是 检测与反应。软件不能仅仅依赖于预防,检测和反应也必须有,预防甚至不是我们首先要考虑的。

建立威胁模型

一个攻击者可以通过多种方式接触系统,要覆盖所有的入侵方式似乎是不现实的,需要准确地确定系统的哪些部分最重要且容易受攻击,优先保护它们。

为了安全性,需要某种针对我们的应用的安全性的宏图,需要知道哪个地方最容易受到攻击,以及如果被攻击会造成怎样的破坏。这些信息反过来可以帮助我们区分哪些弱点是需要关注而哪些是可以忽略的。这些资源一般被称为一个威胁模型。

如果没有威胁模型,想知道需要花多少时间和精力来防范一个指定的企业系统可能面对的某个潜在的安全攻击是不可能的。

做不安全假设

建立安全系统的一个特殊要点:你必须假设每一样东西都是不安全的,包括你正在建立的那些部分,直到你确定系统可被入侵和攻击的安全疑点都被清除了。

两条关于企业级安全性的定律:

  • 假设一个构件是不安全的,除非你能证明;
  • 一个构建永远不能被证明是安全的。

总是假设每件东西都是不安全的,假设人们会通过其中一些构件攻破系统。

现实世界中的安全系统都使用深度防御策略。它们假设在每个层次上,防御都会被攻破,因此需要另一种防御来以防万一。

我们不能阻止一个攻击者,预防本身永远不能保证提供一个安全的系统,目标是拖延攻击者,以使入侵检测系统和警报管理员可以检测并采取合适的反应措施。

这一条的结论是采用 最低权限原则。

总是验证用户输入

如何处理用户输入是建立一个安全系统的关键,任何一个系统接受的用户输入都是攻击者进入系统的通道。

必须假设客户端验证逻辑没有被执行,对于每个用户输入的提交,都要严格地进行一序列的验证校验以保证用户输入中的所有基于输入的攻击都被过滤了。

(数据与代码分离)


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

计算 一个点 附近的地方

问题

随着地理位置的应用普及,越来越多类似计算 一个点 附近的酒店的需求。比如,显示当前位置2000米范围内的 7 天酒店。

一般来说,在系统里都有一张表,存储了每个 7 天酒店的位置信息,表结构可以简化为三个字段 tb_location(ID, lng, lat),其中 lng:表示经度;lat:表示纬度,这两个字段都是数值类型的,且建了索引。

现在要把 2 公里范围内的 7 天酒店找出来,有的人写的 SQL 语句大概是这样的:
select id from tb_location where calc_distance(lng, lat, curLng, curLat) < 2000

这个语句的问题是没法利用索引,需要全表扫描,计算每一条记录与当前位置的距离,效率很低。

优化

用一张图来说明优化策略:
附近地点计算优化

继续阅读

关于 RESTful 的一点思考

RESTful 是什么?

来自《Spring 实战》:

REST 组成:表述性、状态、转移。

REST 是将资源的状态以最合适的形式从服务器转移到客户端。

RESTless

RESTless 是面向行为的,而不是面向资源的。比如:
http://host:port/servletContext/showUser.action?id=123

在这个 URL 里,servletContext 是 servlet 上下文路径;showUser.action 是控制器 URL 模式,其中 show 是动词,是一种行为;id=123 是标识符。

RESTful URL

比如:http://host:port/servletContext/users/123

在这个 URL 里,users 是资源类型(名词),123 是特定的 user。

这个 URL 并不做任何事情,只是标识了一个资源,而对这个资源做什么是由 HTTP 请求的方法决定的。

RESTful URL 的一些特性:

  • 不仅定位资源,还可以唯一标识这个资源;
  • 有层次,从左到右读时,是一个从抽象到具体的过程;
  • 对于服务器端应用,路径是参数化的。

继续阅读

Akka Actor 生命周期

本文主要展示 Akka Actor 的生命周期管理和默认的监管策略。基于 Akka 2.3.4,Scala 2.11。

Akka Actor 生命周期钩子

Actor 实例化后就由 Actor 运行时调度执行。

Akka Actor 定义了下列的生命周期回调钩子:

  • preStart:在 actor 实例化后执行,重启时不会执行。
  • postStop:在 actor 正常终止后执行,异常重启时不会执行。
  • preRestart:在 actor 异常重启前保存当前状态。
  • postRestart:在 actor 异常重启后恢复重启前保存的状态。

继续阅读

Scala Trait 堆叠特性

以一个简单的例子展示 Scala Trait 线性堆叠的特性:

package net.coderbee.scala

trait BaseTrait {
    def action
}

trait Trait0 extends BaseTrait {
    abstract override def action {
        println("action at Trait0")
        super.action
    }
}

trait Trait1 extends BaseTrait {
    abstract override def action() {
        println("action at trait1")
        super.action // 如果某个Trait不调用父类的方法,则会中断Trait的调用栈,这可用以实现过滤
        println("rollback at action at trait1")
    }
}

trait Trait2 extends BaseTrait {
    abstract override def action() {
        println("action at trait2")
        super.action
    }
}

trait BreakTrait extends BaseTrait {
    abstract override def action {
        println("i am break trait, end here ")
    }
}

class ClassWithTrait extends BaseTrait {
    def action() {
        println("action at ClassWithTrait")
    }
}

object StackTrait {

    def main(args: Array[String]) {
        val tr012 = new ClassWithTrait with Trait2 with Trait1 with Trait0
        tr012.action // 从右往左调用 trait 的抽象实现,最后调用本类的。如果中间某个抽象方法没有调用超类的  super 方法,则不会调用到本类

        println
        val tr02 = new ClassWithTrait with Trait2 with Trait0
        tr02.action

        println
        val trBreak = new ClassWithTrait with Trait2 with BreakTrait with Trait1 with Trait0
        trBreak.action
    }
}

输出结果:

继续阅读