Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

春なのでSpringへの移行ガイド

こんにちは、開発ユニットbcです。

みなさんご存知かとは思いますが、Seasar2のサポートが2016年9月26日を以って終了します。
弊社の運営するSUPERDELIVERYでも一部の機能でSeasar2を現在も利用しています。

今回はSeasar2からの移行を検討している方の参考になればと思い、以前に行った移行の事例を紹介させていただきます。

2014年にSUPERDELIVERYの商品登録機能をリニューアルした際、弊社ではSeasar2からSpring Framework(4系)へ移行することを選択しました。
選択の理由はプロダクトの開発が停滞していないことと、Seasar2からの移行のしやすさでした。
なぜ移行がしやすいかというと、Seasar2の各機能を代替する機能がSpring Frameworkにも備わっているからです。

ではまずはSeasar2とSpring Frameworkの構成の比較から。

構成の比較

移行前 移行後
Controller S2Struts Spring MVC
DI Seasar2 Spring Framework
DAO S2Dao Spring Data

このようにSeasar2の機能の移行先がSpring Frameworkにもあることが分かります。
では、商品一覧表示の機能を例にしてどのように移行するかを見ていきましょう。

移行前と移行後の実装を比較

移行前のシステムでは、コンポーネント毎に設定ファイルへ記述するスタイルで開発していたので、
新たに機能の追加がある場合は以下のファイルの追加が必要でした。
  1. XXXAction.java
  2. XXXActionImpl.java
  3. XXXService.java
  4. XXXServiceImpl.java
  5. XXXDao.java
  6. XXX.dicon
  7. struts-config-XXX.xml
■ProductSearchAction.java
public interface ProductSearchAction {
  String execute();
}
■ProductSearchActionImpl.java
public class ProductSearchActionImpl implements ProductSearchAction {
  private ProductSearchService productSearchService;
  private List<Product> products;
  private ProductSearchForm searchForm;
	
  public String execute() {
  products = productSearchService.findProducts(searchForm.createProductSearchCondition());
  return "success";
  }

  public List<Product> getProducts() {
  return products;
  }

  public void setProductSearchService(ProductSearchService productSearchService) {
  this.productSearchService = productSearchService;
  }

  public void setSearchForm(ProductSearchForm searchForm) {
  this.searchForm = searchForm;
  }
}
■ProductSearchService.java
public interface ProductSearchService {
List<Product> findProducts(ProductSearchCondition condition);
}
■ProductSearchServiceImpl.java
public class ProductSearchServiceImpl {
private ProductDao productDao;

public List<Product> findProducts(ProductSearchCondition condition) {
return productDao.findByStatusCode(condition.getStatusCode());
}

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
}
■ProductDao.java
public interface ProductDAO {
  @Query("status_code = ?")
  List<Product> findByStatusCode(Integer statusCode);
  void insert(Product product);
  void update(Product product);
  void delete(Product product);
}
■action.dicon
<components>
... <component name="productSearchAction" class="jp.ne.raccoon.action.product.impl.ProductSearchActionImpl" instance="request"> </component>
... </components>
■service.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.service.product.impl.ProductSearchServiceImpl">
  </component>
  ...
</components>
■dao.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.dao.ProducDao">
  </component>
  ...
</components>
■struts-config-product.xml
<struts-config>
  <action-mappings>
... <action path="/product/search" type="jp.ne.raccoon.action.product.ProductSearchAction" parameter="execute"> <forward name="success" path="/product/search.html" /> </action>
... </action-mappings> </struts-config>
  1. DIはdiconファイルにコンポーネントを個別に定義して、Seasar2のセッターインジェクションを利用していました。
  2. リクエストのマッピングは、struts-config.xmlにaction-mappingsをaction毎に個別に設定していました。
 
移行後は以下のような実装になりました。
■ProductSearchController.java
@Controller
public class ProductSearchController {
@Autowired private ProductSearchService productSearchService; @RequestMapping(value = "/search", method = RequestMethod.GET) public String execute( @Valid @ModelAttribute("searchForm") ProductSearchForm searchForm, BindingResult bindingResult, Model model) {
model.addAttribute(
"products",
productSearchService.findProducts(searchForm.createProductSearchCondition()));
return "product/search"; } }
■ProductSearchServiceImpl.java (※interfaceは移行前と同じなので割愛してます。)
@Service
public class ProductSearchServiceImpl implements ProductSearchService {
@Autowired private ProductRepository productRepository; public String findProducts(ProductSearchCondition condition) { return productRepository.findByStatusCode(condition.getStatusCode()); } }
■ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
  List<Product> findByStatusCode(Integer statusCode);
}

移行前と比べると以下のように実装を置き換えることができました。
Action → Controller
Service → Service
Dao → Repository
diconファイル → @Controller @Service等のアノテーション
struts-config → @RequstMapping
  1. 設定ファイルを必要とせず、Springのアノテーションで コンポーネントの定義とDIを行うようになりました。
  2.  @RequestMappingを利用することでリクエストメソッドの指定やパラメーターの受け取り方等がより柔軟な設定を書けるようになりました。
  3. Daoのinsert/update/deleteのメソッドは、継承元に定義されているのでメソッドを用意する必要はなく、@Queryに相当する物はfindByXXXの部分の命名規則によってwhere句が生成されます。※where status_code = ? が生成されます。
これだけで、新機能の追加に必要な作業がだいぶ軽減されました。
順調に移行できそうでしたが、問題点もありました。

問題点

一つ目が、sqlファイルの外部化です。
S2Daoの資産として、参照系の動的なsqlは外部ファイル化していました。 
Springのプロダクトでは対応している物が見当たらず、可能ならば慣れ親しんだ形で移行したいという思いもあって、Mirageというライブラリを導入することにしました。 

■ProductDao_findProducts.sql
//日付の範囲指定と商品の状態を検索条件で指定できることを例としてます。
select
  product_code
from
  product
/*BEGIN*/ where /*IF dateFrom != null*/ and created_at >= /*dateFrom*/ /*END*/ /*IF dateTo != null*/ and created_at < /*dateTo*/ + 1 /*END*/ /*IF productStatus !=null*/ and product_status = /*productStatus*/
 /*END*/
/*END*/
■ProductDao.java
@Component
@Transactional(readOnly = true)
public class ProductDaoImpl implements ProductDao {
  @Autowired
  private SqlManager sqlManager;

  public Product findProducts(SearchCondition condition) {
//上記sqlファイルを指定 SqlResource sqlResource = new ClasspathSqlResource("ProductDao_find_products.sql"); return sqlManager.getResultList(Product.class, sqlResource, condition); } }
mirageで実装したDaoも@ComponentでSpringのコンポーネントとして定義しました。
必要に応じてspringDataとmirageを使い分けるようにして、移行前の資産も活かすことができました。

二つ目がSeasar2とSpringでのコンポーネントのライフサイクルの違いでした。
S2Strutsの時は、Actionはリクエスト単位で作成していましたが、SpringMVCのControllerでは、デフォルトがシングルトンなため、リクエストやセッション等の短いライフサイクルのコンポーネントのDIができないということでした。
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST) public class XXXController { }
@Scopeの設定でライフサイクルを変えることで対応はできますが、全部のControllerに書くのは・・・ということで、以下のようなアノテーションを作成してライフサイクルをリクエストにしたいControllerに指定するようにしました。
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public @interface RequestScopeController {
}
利用する側のController
@RequestScopeController
public class XXXController {
}
これで、リクエストやセッションのライフサイクルのコンポーネントをDIしても問題なく動作するようになりました。

まとめ  

局所的ではありますが、以上がSeasar2から移行した時の内容です。いかがだったでしょうか。
Seasar2のサポート終了をきっかけに新しいことに挑戦していきたいですね。

ローカルUnboundによる名前解決の冗長化の検証


こんにちは!はんだです。
今日はDNSサーバの可用性向上についてのエントリです。

DNSサーバの障害時の影響を防ぐ方法として、
単純にDNSサーバを複数設置して/etc/resolv.confに記入するという方法がありますが、
その方法には、タイムアウトの待ち時間という落とし穴があります。

/etc/resolv.conf
nameserver 192.168.0.11  //DNS1のIPアドレス
nameserver 192.168.0.12  //DNS2のIPアドレス

のように設定したとして、
1行目のDNS1が落ちた状態の場合、2行目のDNS2に移行するまでに、設定次第ですが問い合わせの度に毎回1~数秒のタイムアウトが発生します。名前解決の頻度が高いサービスの場合、この僅かな待ち時間がサービス全体へのパフォーマンスに影響してきます(この問題については[24時間365日]サーバ/インフラを支える技術(技術評論社)の 「DNSサーバの冗長化」の項が詳しいです)。

この問題への対策として、LVS+Keepalivedなどを利用して冗長性を高める方法がありますが、それに加え

複数のDNSサーバをローカルのUnboundの参照先として指定することで冗長性を高める

という方法を検証してみました。

(なお、今回の記事ではLVS構成の設定及び検証については言及しませんので、その部分については他の解説記事などをご参照下さい)

検証環境

検証用の環境として以下を準備しました。
sv-web01       192.168.0.1       Webサーバ。Unboundを導入
sv-dns-lvs01     192.168.0.11       node01とnode02でLVS。Keepalivedで死活監視
  sv-dns-node01  192.168.0.101     BIND
  sv-dns-node02  192.168.0.102     BIND
sv-dns-lvs02     192.168.0.12       node03とnode04でLVS。Keepalivedで死活監視
  sv-dns-node03  192.168.0.103     BIND
  sv-dns-node04  192.168.0.104     BIND

Unbound



sv-web01にはUnboundをインストールし、/etc/resolv.confには以下のようにnameserverを設定します。
nameserver 127.0.0.1       //ローカルのUnbound
nameserver 192.168.0.11    //sv-dns-lvs01
nameserver 192.168.0.12    //sv-dns-lvs02

Unboundの設定

Unboundとは、
  • シンプルな設計になっており高速で動作する。BINDより処理性能が良い(2~3倍)
  • 設定ファイルは1つのみ(unbound.conf)で内容もシンプルなため、設定が容易
  • キャッシュ汚染に対する耐性を強化した設計&DNSSECをサポート
  • デフォルト設定で高いセキュリティが確保された状態で動く
という特徴を持った、新定番となっているDNSキャッシュサーバです。

デフォルトの設定でもキャッシュサーバとして問題なく動作しますが、今回は社内の権威DNSサーバへ問い合わせを行いたいため、stub-zoneの設定を行います。
stub-zoneは、特定のドメインの名前解決について、指定したDNS権威サーバに問い合わせを行うための設定です。

/etc/unbound/unbound.confで、参照先DNSサーバとしてsv-dns-lvs01とsv-dns-lvs02を指定します。
 stub-zone:
        name:"example.com"
        stub-addr:192.168.0.11
        stub-addr:192.168.0.12
stub-zone:
        name: "0.168.192.in-addr.arpa"
        stub-addr:192.168.0.11
        stub-addr:192.168.0.12
Unboundはデフォルトではプライベートアドレスの問い合わせを行わないので、以下の設定を追加します。
private-domain: "example.com"
(中略)
local-zone: "0.168.192.in-addr.arpa" transparent

また、特定のドメインについて、指定したDNSキャッシュサーバに再帰問い合わせを行わせるときにはforward-zoneを設定します。
forward-zoneは、nameオプションに "." を指定することで、全ての問い合わせを転送することもできます。
自社ドメイン以外は別のDNSサーバ(外部DNSなど)を指定して直接問い合わせを行わせるような場合は、こちらで設定します。
forward-zone:
  name: "."
  forward-addr: 203.0.113.111

検証したいこと 

1. stub-zoneに複数DNSサーバを記述した場合、どのような問い合わせ順序になるか。先に書かれているものが優先なのか、ラウンドロビンなのか。

2. stub-zoneに書いたDNSサーバのうち1つが利用不可になった場合に、どのような挙動になるか。

3. 利用不可になった理由が、サーバ自体がダウンした場合と、DNSサービスが利用不可になった場合では、挙動に違いはあるか。resolv.confでは、サーバ自体のダウンではなくDNSサービスが落ちていた場合にタイムアウトまでに長くかかることがあるが、Unboundではどうか。

検証手順

0.事前準備
テスト用のスクリプトを作成します。sv-web01から他のサーバにnslookupを行い、最後にunboundのキャッシュを削除するというだけの単純なものです。
SERVER_LIST="sv-web02 sv-web03 sv-db01 ・・・・・ sv-db20"
while : ; do

    for SERVER in $SERVER_LIST; do
        echo $(date "+%F %H:%M:%S.%N") $(nslookup $SERVER | head -n 1)
    done

    unbound-control -q flush_zone example.com
    sleep 1

done | tee test.log
このスクリプトを実行しながらサーバやサービスを落として、どのサーバで名前解決が行われているかを確認します。
DNSサーバに何の問題も起こっていない時点では以下のようなログになります。
127.0.0.1、つまりローカルのUnboundで名前解決が行われているのがわかります。
test.log

2016-03-23 10:01:47.710318602 Server: 127.0.0.1
2016-03-23 10:01:47.720166753 Server: 127.0.0.1
2016-03-23 10:01:47.730168114 Server: 127.0.0.1
2016-03-23 10:01:47.739930041 Server: 127.0.0.1
2016-03-23 10:01:47.750394444 Server: 127.0.0.1
2016-03-23 10:01:47.760969879 Server: 127.0.0.1
2016-03-23 10:01:47.770984202 Server: 127.0.0.1
2016-03-23 10:01:47.780918869 Server: 127.0.0.1
2016-03-23 10:01:47.791561248 Server: 127.0.0.1
2016-03-23 10:01:47.802000414 Server: 127.0.0.1
2016-03-23 10:01:47.812778410 Server: 127.0.0.1
2016-03-23 10:01:47.823340386 Server: 127.0.0.1
2016-03-23 10:01:47.833941263 Server: 127.0.0.1
2016-03-23 10:01:47.844795349 Server: 127.0.0.1



検証1. 
問い合わせ順序の確認

各LVSサーバでDNSサービスへの接続状態を確認しながら検証用スクリプトを実行してみます。
[root@i-dns-lvs01 ~]# watch ipvsadm -Ln

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
UDP 192.168.0.11:53 rr
-> 192.168.0.101    Route     0 0 271 
-> 192.168.0.102     Route    0 0 272
[root@i-dns-lvs02 ~]# watch ipvsadm -Ln

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
UDP 192.168.0.12:53 rr
-> 192.168.0.103    Route     0 0 269 
-> 192.168.0.104     Route    0 0 271 

InActConn(一定時間以内の接続数)がほぼおなじ数字で推移しています。
結論:stub-zoneに複数のDNSサーバを記入した場合、順番に均等に問い合わせが割り振られる 。


検証2.ローカルのUnboundのstub-addrに登録してあるsv-dns-lvs0xのどちらかが落ちた場合


上記スクリプトを実行した状態で、片方のLVS(sv-dns-lvs01)を落としてみます。
[root@sv-dns-lvs01 ~]# service network stop
2016-03-23 11:02:39.654407380 Server: 127.0.0.1
2016-03-23 11:02:39.666387084 Server: 127.0.0.1
2016-03-23 11:02:39.678305469 Server: 127.0.0.1
2016-03-23 11:02:39.690497052 Server: 127.0.0.1
2016-03-23 11:02:39.702769957 Server: 127.0.0.1
2016-03-23 11:02:39.714040967 Server: 127.0.0.1
2016-03-23 11:02:39.725401558 Server: 127.0.0.1
2016-03-23 11:02:39.736123310 Server: 127.0.0.1
2016-03-23 11:02:39.746930229 Server: 127.0.0.1
2016-03-23 11:02:39.758402234 Server: 127.0.0.1
2016-03-23 11:02:39.769120682 Server: 127.0.0.1
2016-03-23 11:02:39.781072673 Server: 127.0.0.1
2016-03-23 11:02:41.296289719 Server: 127.0.0.1
2016-03-23 11:02:41.308759832 Server: 127.0.0.1
2016-03-23 11:02:41.422097623 Server: 127.0.0.1
2016-03-23 11:02:41.434119357 Server: 127.0.0.1
2016-03-23 11:02:42.849200158 Server: 127.0.0.1
2016-03-23 11:02:42.861381999 Server: 127.0.0.1
2016-03-23 11:02:42.873155039 Server: 127.0.0.1
2016-03-23 11:02:42.884901850 Server: 127.0.0.1
2016-03-23 11:02:42.896203445 Server: 127.0.0.1
2016-03-23 11:02:42.906861984 Server: 127.0.0.1
2016-03-23 11:02:42.917331042 Server: 127.0.0.1
2016-03-23 11:02:42.927999125 Server: 127.0.0.1
2016-03-23 11:02:42.938445488 Server: 127.0.0.1
2016-03-23 11:02:42.948876159 Server: 127.0.0.1
2016-03-23 11:02:42.959507049 Server: 127.0.0.1
2016-03-23 11:02:42.970042488 Server: 127.0.0.1

LVSサーバを落としたタイミングで若干処理スピードの低減が発生しましたが、ほぼタイムラグなくsv-dns-lvs01を切り離して引き続きUnboundで名前解決を行ってくれるようです。

結論:stub-addrとして指定した複数のDNSのうち一つが落ちた場合でも、Unbound内で切り離しをして名前解決が続行される。



検証3. sv-dns-lvs0x自体が落ちている場合と、sv-dns-lvs0xにぶらさがっている2つのnodeの両方のDNSサービスが利用不可の場合とで挙動は変わるか


LVS用のサーバが落ちた場合、Unboundがうまく切り離してくれることがわかりました。
では、LVSサーバではなく、node側のDNSサービスが両方落ちた場合はどうでしょうか。

sv-dns-lvs01で利用している、sv-dns-node01とsv-dns-node02の両方で、namedサービスを停止してみます。
[root@sv-dns-node01 ~]# service named stop 
[root@sv-dns-lvs01 ~]# ipvsadm -Ln 

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
UDP 192.168.0.11:53 rr
-> 192.168.0.101    Route     0 0 0 
-> 192.168.0.102     Route    0 0 0 
2016-03-23 11:26:07.253176732 Server: 127.0.0.1
2016-03-23 11:26:07.263412675 Server: 127.0.0.1
2016-03-23 11:26:07.375477267 Server: 127.0.0.1
2016-03-23 11:26:07.385581955 Server: 127.0.0.1
2016-03-23 11:26:07.395923909 Server: 127.0.0.1
2016-03-23 11:26:07.507452435 Server: 127.0.0.1
2016-03-23 11:26:09.022468808 Server: 127.0.0.1
2016-03-23 11:26:09.635581100 Server: 127.0.0.1
2016-03-23 11:26:09.646748991 Server: 127.0.0.1
2016-03-23 11:26:10.712805036 Server: 127.0.0.1
2016-03-23 11:26:10.725051359 Server: 127.0.0.1
2016-03-23 11:26:10.734853913 Server: 127.0.0.1
2016-03-23 11:26:10.744873105 Server: 127.0.0.1
2016-03-23 11:26:10.754697333 Server: 127.0.0.1
2016-03-23 11:26:10.764829874 Server: 127.0.0.1
2016-03-23 11:26:10.877325395 Server: 127.0.0.1
2016-03-23 11:26:10.887888348 Server: 127.0.0.1
2016-03-23 11:26:10.898024415 Server: 127.0.0.1
2016-03-23 11:26:10.907979399 Server: 127.0.0.1
検証2と同様、若干の処理速度低下はありますが、その後は引き続きUnboundでの名前解決が継続されました。

結論:LVS自体が落ちている場合とサービスが使用不可の場合で挙動は変わらない

まとめ 

検証により、DNSサーバ障害時のパフォーマンスへの影響をローカルUnboundを導入することによりより減らすことができるだろうという結論が得られました。
ここで一度、Unboundを導入することのメリット・デメリットを整理したいと思います。

ローカルにUnboundを導入するメリット
・サーバ障害時も、キャッシュに保存されている分はTTL内(社内サーバなら1時間)であればローカルで名前解決ができる。
・DNSサーバを冗長化しておいた場合に、1台のみの障害であればresolv.confに列記するだけの場合よりもパフォーマンスへの影響を小さくできる。
・ローカルのネットワーク流量やDNSサーバの負荷を抑制できる。
・ローカルで名前解決ができることによるパフォーマンス向上が期待できる。

ローカルにUnboundを導入するデメリット(懸念事項)
・導入コスト(各サーバへの設定)
・運用コスト(local-dataを使う場合はサーバ追加時のlocal-dataの再配布、ドメイン&セグメント追加時のunbound.conf設定修正)
・キャッシュが新しいトラブルの原因になったり、トラブル時の切り分けがしづらくなる可能性
・サーバ負荷軽減やパフォーマンス向上については、名前解決の問い合わせが少ないサーバや、新しい名前解決が多いサーバの場合には、効果が小さい。

これを踏まえ、弊社では、
・TTL内での名前解決が多く発生し、キャッシュにより問い合わせが大きく減らせる
・DNS障害時のタイムラグがサービスのパフォーマンスに影響を与えることが想定できる
など、Unbound導入によるメリットが大きいサーバから順に導入していくことになりました。
また、導入・運用のコストについては、AnsibleやFabricといった自動化ツールを導入することである程度カバーできると考えています。

参考URL

・5分でわかるUnbound http://gihyo.jp/admin/feature/01/unbound/0001
・Unboundサーバ運用Tips http://gihyo.jp/admin/feature/01/unbound/0004
・キャッシュサーバの設定 http://dnsops.jp/event/20140626/cache-config.pdf

Zipファイルを紙とペンで解凍してみた

こんにちは。ほそかわです。

社内勉強会用に、埋めていくだけでzipファイルの解凍ができるプリントを作りました。

普段、zipファイルの解凍はコンピュータにやってもらっていますが、
この記事を読みながらご自身の手を使って紙とペンでやってみましょう。

アルゴリズムとデータ構造を生で感じるチャンスです。

準備

これから使うプリントをダウンロードして印刷しましょう。

「ZIPローカルファイルヘッダ」、
「ZIPセントラルディレクトリファイルヘッダ」、
「ZIPセントラルディレクトリ終端レコード」

記入してZIPの解凍を進めていくプリントです。

「ZIPファイルデータ」
この記事で解凍を行うzipファイルのバイナリデータです。
左側に書いてあるのは16進数で表されたバイナリデータです。主にこちらを使います。
16進数で書かれている行名と列名を足すと何byte目のアドレスにあるデータかが分かります。
右側に書いてあるのは文字列データが簡単に調べられるASCIIで表されたバイナリデータです。

「16進数表」
計算の手間を省くためのものです。

全て印刷して筆記用具も用意できたら準備はOKです。

今回用意したZIPファイルデータは、データ圧縮が行われていない特殊なzipファイルになってます。
圧縮を行わないzipファイルに意味なんてないと感じるかもしれませんが、圧縮に関する仕様は独立しているのでこの記事を読んだ後に別に勉強できます。

無圧縮のzipファイルはzipコマンドでも作れます。

zip -0 file.zip file.txt

プロトコルのバージョンによって違いがあるかもしれないのでこの記事ではこちらで用意したファイルを使います。

概要

Zipファイルの中にはどんなデータが入っているのか見てみましょう。

ファイル全体の概要を図にしました。

zip_overview_multi_file
Zipファイルはヘッダとファイルエントリ、終端レコードで構成されています。

ヘッダには2種類あります。

  • ZIPローカルファイルヘッダ
  • ZIPセントラルディレクトリファイルヘッダ

ZIPセントラルディレクトリファイルヘッダは、ZIPファイルの最後にあるZIPセントラルディレクトリという領域に入っています。そして、ZIPセントラルディレクトリの一番最後にアーカイブ全体についての情報が書いてある終端レコードが付いていてます。

ヘッダにはzipファイルに格納したファイルについてファイル名などの情報が書いてあり、ファイルエントリには格納したファイルの中身が入っています。

図のzipファイルにはA,Bの2つのファイルが格納されています。2種類のヘッダとファイルエントリがファイル数だけ繰り返されています。

ネタバレになってしまいますが繰り返しがあると手間が増えるばかりなので、今回使うzipファイルに格納されているファイルは一つです。

バイナリデータ

Zipファイルは普段扱っているテキストデータよりもコンピュータ向けなバイナリデータとして保存されています。

バイナリデータを扱う為に、16進数、2進数の変換方法とエンディアンについて知っておきましょう。

基礎的な知識なのでWikipediaをみてください。

2進法

16進数

エンディアン

エンディアンは様々なことが書いてありますが、zipファイルを扱うのにはリトルエンディアンとビックエンディアンのデータが読めるようになっていれば十分です。

リトルエンディアンでは1byteをかたまりとして並び順を逆にします。

AB CD EF 12

であれば

12 EF CD AB

となります。

例えば、16進数で1B58という数値をリトルエンディアンで表すと

58 1B

となります。

ビックエンディアンは並び順を変えないもので、

AB CD EF 12

であれば、そのまま

AB CD EF 12

となります。

ZIPセントラルディレクトリ終端レコード

これから紙とペンを使ってzipを解凍していきます。

まずは、アーカイブ全体の情報を得るために、セントラルディレクトリの終端レコードに記録してある情報を調べましょう。

使うプリントは、「ZIPセントラルディレクトリ終端レコード」です。

概要で説明したように、終端レコードはzipファイルの最後の領域にあります。

ZIPの仕様として終端レコードの頭には、

50 4B 05 06

というシグネチャをつけることになっています。

終端レコードはファイルの末尾にあるので末尾から開始地点を表すシグネチャを探すことで、終端レコードの領域を知ることができます。
zipファイルデータの末尾からシグネチャを探しましょう。

アドレス0x5Cに見つかりました。

そこからセントラルディレクトリヘッダの終端レコードが始まってます。
次のアドレスから順番にプリントに書き込んでいくと終端レコードの解凍を進められます。

四角い枠一つが1byteに対応してます。

入力欄解説
中には1byteのデータを書き込みます。

シグネチャは例としてリトルエンディアンで埋めてあります。

次の手順で全て埋めてみてください。

  1.  16進数で読み、数値データはリトルエンディアンで、(文字列)と書いてある文字列のデータはビックエンディアンで四角い枠の欄に記入する。

  2.  数値データは、16進数のデータを10進数に直して下線が引いてある欄に記入する。文字列のデータは1byteごとにASCII文字に変換してある右に表の同じ位置の文字を探して記入する。

(厳密には、文字列のデータもリトルエンディアンになっています。並び替えを行わないビックエンディアンとして扱えている理由は、1文字の1byte単位で保存が行われているからです。リトルエンディアンは1byteをかたまりとして並び替えるものなので、1byteのデータでは並び替えが行われず、リトルエンディアンになる並び替えが行われてないように見えています)

最初なので答えを載せます。

ZIPセントラルディレクトリ終端レコード_答え

全ての記入が終わったら細かく見て行きます。

セントラルディレクトリレコードの合計数

セントラルディレクトリレコードの数はファイルの数と同じになっています。
このzipファイルに含まれるファイルは一つだけだと分かります。

セントラルディレクトリの開始位置のオフセット、セントラルディレクトリのサイズについて

セントラルディレクトリの開始位置のオフセットからは、ファイルの先頭から見てどのアドレスでセントラルディレクトリが始まるのか、セントラルディレクトリのサイズからはどこまで続いているのかが分かります。
セントラルディレクトリは、先頭から39byte目のアドレス0x27で始まって53byte後のアドレス0x5Bで終わっているということがわかります。

Zipファイルコメントと長さについて

このzipファイルにはコメントがないため長さには0が入っています。
コメントは可変長のデータなのでここで長さを定義してそのバイト数だけコメントとして扱うという作りになってます。

その他の項目については、今では使われない機能が多いので割愛します。
興味があったら調べてみてください。

ZIPセントラルディレクトリファイルヘッダ

次は、セントラルディレクトリファイルヘッダです。
使うプリントは、「ZIPセントラルディレクトリファイルヘッダ」です。

終端レコードの情報から、セントラルディレクトリはアドレス0x27で始まっているとわかりました。
セントラルディレクトリファイルヘッダのシグネチャは、

50 4B 01 02

です。

探してみると、終端レコードで書いてあったようにアドレス0x27にあります。

終端レコードの時と同じようにシグネチャの次のバイトからプリントを埋めていきます。

zipでは、日付、時刻のデータがバイト単位で分けられてません。
2byteの領域に3つのデータが入っているので、
一度、2byteを繋げた2進数に変換してから区切り直して、10進数に変換します。

次の手順で行います。

  1. リトルエンディアンで2byte分を2進数に変換して一桁ずつ下線の上に書く
  2. 縦線で区切られているところで分けて2進数から10進数変換して、次の行に書き込む
  3. 秒は2倍、年は1980を足す

このやり方も初めてなので答えを載せておきます。

日時入力欄解説

これでファイルの最終変更日時がわかります。

これも全ての記入が終わったら細かく見ていきます。

圧縮メソッド
ファイルエントリがどのプロトコルで格納されたかが書いてあります。
今回は無圧縮なので0が入っていますが8のDeflateが一般的です。

Wikipedia - Deflate

ファイルの最終変更時刻、最終変更日付
格納したファイルの最終変更日時です。
年は1980の下駄を履かせるので1980年からが使えます。
そして、なんと、秒はビットが足らず2秒刻みで丸められているので2倍します。

zipで圧縮を行ったファイルは最終変更日時の秒が保障されないことをご存知だったでしょうか。いつも使っているものでも意外な発見があるものなんですね。

CRC-32
格納前のファイルエントリで計算したCRC-32が入っています。

圧縮サイズ
格納後のサイズです。ファイルエントリのサイズが書いてあります。

非圧縮サイズ
格納前のファイルサイズです。

ファイル名の長さ、ファイル名
Zipファイルコメントと同じようにまず長さが定義されていて、その長さ分のファイル名データがあります。

ローカルファイルヘッダの相対オフセット
ファイル一個につき、セントラルディレクトリファイルヘッダとローカルファイルヘッダが一つずつあります。

対応するローカルファイルヘッダのアドレスが書いてあります。
最初のファイルのローカルファイルヘッダのアドレスなので0が入っています。

数が多いので他は割愛します。
調べてみてください。

ZIPローカルファイルヘッダとファイルエントリ

ローカルファイルヘッダはZIPセントラルディレクトリファイルヘッダと同じやり方で埋めていけます。
ローカルファイルヘッダの次のアドレスからファイルサイズのバイト数分だけがファイルエントリと呼ばれるファイルの中身のデータになってます。

圧縮が行われている場合には、ヘッダから得た圧縮プロトコル情報をもとにファイルエントリのデコードを行います。今回は無圧縮なので飛ばせます。

ファイルエントリをこれまでヘッダから得た情報をもとにファイルとして保存すればこのファイルは解凍されます。

今回は、

  • ファイルの最終変更日は「2016年1月26日 13時27分36秒」
  • ファイル名は「raccoon」
  • ファイルの中身は「CA FE」

というファイルを作り、ファイルエントリをそのまま中に書き込めばこのzipファイルの解凍は成功です。

「CA FE」とは16進数で使われるA~Fまでの文字という制約で考えた単語「Cafe」です。

最後に

お疲れ様でした!

Zipファイルが身近に感じられるようになったのではないでしょうか。

この記事は最短ルートでzipファイル概要を理解し解凍を体験できることを目標に書いたので多くの仕様を省略しています。
次のステップとして、圧縮アルゴリズムを勉強すると圧縮が行われている普通のzipファイルも解凍できるようになります。他にもパスワード付きや分割など多くの仕様があります。そして、反対のことを行えば圧縮もできます。

ZIPファイルフォーマットは様々なところで使われています。ZIPの知識を生かすことで仕事の幅が広がるかもしれません。このプリントを使って勉強会を開催するのも面白いでしょう。
これからの活躍につながるといいですね。

参考

wikipedia - ZIP

PK-Ware - APPNOTE

[E2Eテスト]メンテナンス自動化~パスワードの定期変更に対応する~

こんにちは。
技術戦略部のやすだです。

以前弊社のCI/CDへの取組みについての記事がありましたが、
今回の記事はその中で触れられていた「Selenium」を利用したE2Eテストのシナリオメンテナンスについてです。

経緯 

 弊社ではSeleniumを使ったE2Eテストを行っています。
 その中で、ログイン後の機能について確認するシナリオがあり、ログインに利用するテスト用アカウントのパスワードをシナリオに直接記述していました。

 しかし、弊社ではセキュリティの観点から、社内で利用している全てのテストアカウント用のパスワードをバッチで定期的に自動変更しています。
 
 シナリオに記述されているパスワードについては、定期変更がされる度に人力で変更していたため、シナリオ側のパスワードを変更しそびれると、ログインできずにシナリオの実行に失敗することになってしまいます。 
 というわけで、今回の要望です。
 

要望

 パスワードが変更されたタイミングでSeleniumシナリオに記述しているパスワードも自動で変更したい!
 

概要

 まずはざっくりとパスワード更新バッチとSeleniumのシナリオについての図です。
スライド2

 DBに保存されるパスワードはハッシュ化したものですので、Seleniumのシナリオで利用するパスワードは生成した直後の平文である必要があります。
 そのため、下記の2点の対応を行います。
  1.[パスワード更新バッチ]ハッシュ化前のパスワードをファイルに出力する
   Seleniumのシナリオでログイン後ページを確認するためだけのアカウントなので、何か特別なことができるわけではありませんが、 平文のパスワードを保存するのでサーバーのアクセスコントロールは適切に設定する必要があります。
  2.[テストシナリオ]ファイルからパスワードを取得する

 上記の対応を行うことで、このようになります。 
指摘対応中_技術ブログネタ

詳細内容

 ということで詳細内容です。
 1.[パスワード更新バッチ]ハッシュ化前のパスワードをファイルに出力する
   生成した直後のハッシュ化前の平文パスワードをファイルに出力します。
   Seleinumのシナリオ内では、データファイルとしてHTMLファイルが扱いやすくなっています。
   文字列をHTMLで出力するだけで特別なことは行っていないため、内容については割愛しちゃいましょう。
   ファイルは下記のような形にしています。
password.html
<html>
  <head></head>
    <body>
       <div id="password">1234</div>
    </body>
</html>

 2.[テストシナリオ]ファイルからパスワードを取得する
   Seleniumのシナリオ内ではjavascriptを記述することができます。
   さらに、storeEvalを使うことで値を保持できるため、異なるページで取得した値を取り扱うことが可能です。
   シナリオ内でのjavascriptの実行については、下記の要領でタグ内に記述することで実行することができます。
<td>
 javascript{"'" + this.page().getCurrentWindow().document.getElementById("password").textContent.toString() + "'";}
</td>

  実際にパスワードを取得する際の記述は下記のようになります。
  • パスワードファイルを開く
<tr>
    <td>open</td>
    <td>社内サーバーに配置したパスワードファイルのpath</td>
    <td></td>
</tr>
  • パスワードを取得し、storeする
<tr>
<td>storeEval</td>
    <td>javascript{"'" +this.page().getCurrentWindow().document.getElementById("password").textContent.toString() + "'";}</td>
    <td>get_pass</td>
</tr>
  • storeしたパスワードを他のページで使う
    storeEvalで格納した値は他のページに遷移した後も保持されます。
    そのため、異なるページでも下記の要領で使いまわすことが可能です。
<tr>
    <td>open</td>
    <td>http://www.superdelivery.com/</td>
    <td></td>
</tr>
<tr>
    <td>type</td>
    <td>name=password</td>
    <td>${get_pass}</td>
</tr>


まとめ

  E2Eテストを自動化する最大のメリットはリグレッションテストのコストや障害の発生を抑えられることだと思っています。
 リグレッションテストのコストや障害の発生を抑えることは、システム変更を容易にすることにつながります。
 システム変更が容易であれば、より良いサービスをスピーディーにお届けすることができます。
 ですが、E2Eテストを自動化する場合、どうしてもシナリオのメンテナンスコストが発生してしまうのが悩みどころとなります。
 今回のようにメンテナンス自体もなるべく自動化することで、E2Eテストのメンテナンスコストという悩みを解決し、最終的により良いサービスをスピーディーにお届けできればというのが個人的な思いです。

CORECを支える技術 ~ログ監視編~ fluentd-AmazonSNS連携

こんにちは。なべです。
CORECの開発を担当しています。
CORECを支える技術 ~チケット駆動編~』に続き、『CORECを支える技術』シリーズの第2弾としてCORECのログ監視について書きたいと思います。

旧ログ監視の問題と対策

当初、CORECのログ監視はログ集約にくっつける形で始まりました。
ログ集約はログファイルを一定時間ごとにバックアップする仕組みで、各サーバーのログをfluentdで集め、1時間ごとに圧縮しS3に格納しています。
ログ監視は、その集約されたログを社内の監視システムで取得し、エラーログがあった場合にメールとIRCで通知するというものです。

この仕組には大きく以下の2つの問題がありました。

  • タイムラグ
    ログの監視をできるタイミングがS3に圧縮ファイルがアップされた後なので、検知するまで1時間のタイムラグが発生する可能性がある。
  • サービスレベル
    ログ監視を社内のサーバーに配置していて、消防点検や停電または、サーバーメンテナンス時に監視できないなどサービスレベルにも問題がある。

これらの問題に対応するためfluentdに集めたタイミングでエラーログを抽出し、AmazonSNS経由でメール通知することにしました。
これにより、社内のサーバーも経由しなくなり、タイムラグの問題・サービスレベルの問題を同時に解消できました。

CORECログ監視概要図

実施内容

この対応で実施したことは以下の4つです

  1. fluentdにプラグイン追加
  2. AmazonSNSに通知用Topic作成
  3. メールBody整形用のERB追加
  4. fluentdの設定ファイル記述

1. fluentdにプラグイン追加

以下の2つのプラグインを追加しました。

fluent-plugin-sns
Amazon SNS通知用
fluent-plugin-rewrite-tag-filter
エラーのフィルタ・レベルごとの分岐処理用
fluentdはrubyで動いているのでRubyistならお馴染みのgemコマンドでインストールできます。
この時fluentdが使っているrubyの環境を指定する必要があります。
特にfluentdにバンドルされたrubyを使用している場合は注意してください。
sudo /usr/lib/fluent/ruby/bin/gem install fluent-plugin-sns
sudo /usr/lib/fluent/ruby/bin/gem install fluent-plugin-rewrite-tag-filter

2. AmazonSNSに通知用Topic作成

Amazonの管理コンソールからSNSサービスを選択し、画面に従ってTopic及びSubscritionを作成します。 特に迷うことはないかと思いますが、注意するポイントは以下の2点です。

  • メールアドレスを登録するとメールがくるのでクリックして認証すること
  • 作成したTopicの権限設定を確認する
    権限設定とは作成したTopicに対して、通知に利用するAWSアカウントがこのトピックに投稿する権限のことです。もし権限がない場合には、[ポリシーの編集]から許可を与えてください。

3. メールBody整形用のERB追加

CORECの場合、fluentdが受け取るレコードは改行が\nにエスケープされていて読みづらいため、テンプレートで改行コードに変換しました。
以下のコードをerbファイルとして保存しておきます。
これだけのコードですが受け取った側の負担がだいぶ違います。特にスタックトレースで威力を発揮します。

<%record.each do |k,v|%>
<%="#{k}:#{v.to_s.gsub(/\\n/,"\n")}" %>
<%end%>

4. fluentdの設定ファイル記述

それでは最後にメインとなるfluentdの設定ファイルの編集をします。
変更/追加のポイントは以下の3点です。

4-a. copyプラグインで処理を2系統に分岐する
4-b. rewrite_tag_filterプラグインでエラーレベルごとにフィルタリングして、新しいタグを設定する
4-c. snsプラグインで通知するためのmatchディレクティブを追加する

それぞれ簡単に説明します。

4-a. copyプラグインで処理を2系統に分岐する

fluentdは最初にマッチしたmatchディレクティブだけを実行しますので、同じレコードに対して複数の処理をしたい場合にはcopyプラグインを使用します。
※copyプラグインは標準のプラグインなのでインストールをする必要はありません。

matchディレクティブと同等の内容をstoreタグに書くことができ、同じレコードに対して処理を複数記述できるようになります。
今回の対応では従来のS3への送信する処理をcopyで分岐し、SNSに送信する処理を追加します。

<match tag.log>
  type copy
  <store>
        従来の処理
  </store>
  <store>
        追加したい処理
  </store>
</match>

4-b. rewrite_tag_filterプラグインでエラーレベルごとにフィルタリングして、新しいタグを設定する

ただ単に前項の「追加したい処理」にSNS送信のディレクティブの内容を書いてしまうと、アプリケーションが出力したログすべて(エラーでないものも含めて)通知されてしまいます。
そこでログの内容によって処理を振り分けるにはrewrite_tag_filterプラグインを使用します。
レコードのキーを指定して、検索する文字列または正規表現を指定し、それにマッチした場合に新しいタグを指定する仕組みです。
今回はログレベルがFATALもしくはERRORの場合にシステム通知がしたかったので、ログレベルが格納されているキー(lebel)に対して、FATAL・ERRORそれぞれルールを作成しました。
複数のルールを設定するにはrewriteruleの後の数字をインクリメントして記述します。

  <store>
    type rewrite_tag_filter
    rewriterule1 level FATAL corec.rails.sns_fatal
    rewriterule2 level ERROR corec.rails.sns_error
  </store>

4-c. snsプラグインで通知するためのmatchディレクティブを追加する

typeにsnsを指定して、内容にはAWSユーザーにはお馴染みのパラメータ群を設定していきます。 sns_body_templateのパスはtd-agentが参照可能な場所にERBを配置して指定してください。

<match corec.rails.sns_fatal>
  type sns
  sns_topic_name for_system_notice_topic
  aws_key_id xxxxxxxxxx
  aws_sec_key xxxxxxxxxx
  sns_endpoint sns.ap-northeast-1.amazonaws.com
  sns_subject [COREC ALERT]corec/rails
  sns_body_template /etc/td-agent/erb/sns_body_template.erb
</match>

fluentd設定ファイルイメージ(全体)

変更前後の設定ファイルは以下のとおりです。
行はだいぶ増えていますが、それぞれの設定内容に難しいところはありませんでした。

  • 設定ファイル:修正前
<match corec.rails>
    S3に送信する設定(省略)
</match>
  • 設定ファイル:修正後
<match corec.rails>
  type copy
  <store>
        従来のS3に送信する設定(省略)
  </store>
  <store>
    type rewrite_tag_filter
    rewriterule1 level FATAL corec.rails.sns_fatal
    rewriterule2 level ERROR corec.rails.sns_error
  </store>
</match>
<match corec.rails.sns_fatal>
  type sns
  sns_topic_name for_system_notice_topic
  aws_key_id xxxxxxxxxx
  aws_sec_key xxxxxxxxxx
  sns_endpoint sns.ap-northeast-1.amazonaws.com
  sns_subject [COREC ALERT]corec/rails
  sns_body_template /etc/td-agent/erb/sns_body_template.erb
</match>
<match corec.rails.sns_error>
 (省略)
</match>

設定ファイル修正後、fluentdを再起動すれば反映されます。
※fluentdの連携をしている場合には送信元のfluentdも再起動することをおすすめします。
 私が設定した時には送信側が受信側を見失ってログの送信が止まってしまいました。

新たな問題

前項までの対応により、旧ログ監視の問題は解消し、 リアルタイムで、かつサービスレベルの高い仕組みができました。
が、新たな問題がいくつか発生しました。

  1. フィルタリングが無くなったのでERROR・FATALのレベルのログが全部飛んでくる
  2. 異常が発生するとガンガンメールがくる
  3. IRCに通知されない

新たな問題 1. ERROR・FATALのレベルのログが全部飛んでくる
→ 対策: 随時チケット化してエラーログの根本原因を潰す!

実はログを検索してメールを飛ばす処理の中でホワイトリストでフィルタリングして、通知するものを絞っていました。
その仕組みが無くなったのでERROR・FATALのレベルのログが全部飛んでくるようになってしまいました。 fluentdでフィルタリングするという選択肢もありましたが、対応の必要のないFATALやERRORを出すシステム側を修正すべきということで、チケット化してエラーの根本対応を実施。

新たな問題 2. 異常が発生するとガンガンメールがくる
→ 対策: 覚悟しておく!

従来のログ監視は1時間に1回ログを検索してメールを飛ばす処理だったため過去1時間に何件エラーが発生していても1通の通知にまとまっていました。
それがリアルタイムでエラー1件毎に通知されるため、異常事態にはメールが大量に送信されてしまいます。
実際に外部ベンダー側にトラブルがあり、復旧までに4時間かかった際に断続的に300通送信されてきました。orz
しかし、異常が発生しているのだから通知はあるべきでそれを止めるのは本末転倒ということで受け入れることにしました。

新たな問題 3. IRCに通知されない
→ 対策:社内で導入を進めている新監視システムへ

メールと同時にIRCに通知して、業務時間中はメールよりも気づきやすいように対策していましたが、IRCは社内サーバーで稼働しているため利用できませんでした。
現状は通知のために旧監視システムと並行稼動をしていますが、実際にはリアルタイムに飛んでくるメールに十分に気づけています。
今後は社内で進めているSlackのサービスを利用した障害対応フローに移行する予定です。
AmazonSNSに通知先を増やすだけで対応できるはずです。

まとめ

  • fluentdにプラグインを追加することで簡単にAmazonSNS経由の通知を実現できました。
    こういったプラグインが充実しているのもfluentdの魅力だと思います。(fluentdの導入については『fluentd(td-agent) の導入』を参照してください。)

  • ログ監視も即時性・対障害性の高いシステムを構築して確実で迅速な検知が必要です!
    ログ監視は最後の砦であり将来発生する障害の芽を発見するアンテナだと思っています。これからもCORECとともにCORECを支える運用システムもブラッシュアップしていきます。

  • 無駄なログは技術負債。根本から除去しましょう!
    無駄なログがたくさん通知されてくると、本来拾わなければいけないログを見落としてしまう可能性があります。そのために通知するログを絞ることは大切ですが、安易にフィルタリングをするのではなく、一つ一つ原因を突き止めて対応すべきところはプログラムで対応し、技術負債を残さないようにしたいですね。

記事検索