Mercurial > hg > Papers > 2018 > nozomi-master
annotate presen/sample.markdown @ 179:a3ee75a897f3
cut slide
author | Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp> |
---|---|
date | Tue, 06 Feb 2018 13:46:47 +0900 |
parents | 074eb76a9184 |
children | 5a0a11b54ab4 |
rev | line source |
---|---|
176 | 1 title: 分散フレームワークChristieの設計 |
2 author: 照屋のぞみ | |
175 | 3 |
177 | 4 # 研究目的(1/2) |
178
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
5 * 当研究室が開発している並列分散フレームワークAliceではスケーラブルな分散プログラムを信頼性高く記述できる環境を実現する |
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
6 * ここで言う信頼性とは定められた環境下で安定して仕様に従った動作を行うことを指す |
177 | 7 * 仕様の記述のしやすさ、可読性、拡張時に仕様変更を抑えられるかも含む |
8 * スケーラビリティとはサービス利用者が増加したとき単純にノードを追加するだけで線形に性能を向上させる能力 | |
176 | 9 |
177 | 10 # 研究目的(2/2) |
11 * 本研究では、Aliceの通信の信頼性を高めるためにNAT越えの機能設計を提案する | |
12 * そしてその実現にはAliceの再設計が必要であることを示す | |
13 * Aliceの問題点を整理し、得られた知見をもとに分散フレームワークChristieの設計を行う | |
176 | 14 |
175 | 15 |
16 # 目次 | |
17 * Aliceの概要 | |
179 | 18 * AliceのNAT越え |
176 | 19 * Aliceの問題点 |
20 * Christieの設計 | |
21 * 他フレームワークとの比較 | |
175 | 22 * まとめ |
176 | 23 * 今後の課題 |
24 | |
25 | |
26 | |
175 | 27 |
28 # Data Segment と Code Segment | |
29 * Aliceではデータを **Data Segment(DS)** 、タスクを **Code Segment(CS)** という単位に分割して依存関係を記述することでプログラミングを行う。 | |
30 * CSはInput DS(入力されるDS)とOutput DS(出力されるDS)を持つ。 | |
31 * CSはkeyで指定されたDSが揃うと実行されるという性質を持つ。 | |
32 ![opt](./images/dsandcs.svg){:width="50%"} | |
33 | |
34 # CodeSegmentの依存関係 | |
35 * データの依存関係にないCSは並列実行される | |
36 * データの依存関係がある場合は Input DS が揃うと順に実行される | |
37 * DSはCSに専有されるためロックの記述を必要としない | |
38 ![opt](./images/dsandcs2.svg){:width="60%"} | |
39 | |
40 # Data Segment と CodeSegment | |
41 * AliceはJavaで実装されており、DSはJava-Object、CSはRunnableに相当する | |
42 * ユーザーが記述する際には CodeSegment.class を継承することでDSを操作するためのAPIを利用して依存関係を記述することができる。 | |
43 | |
44 # Data Segment Manager | |
45 * DS の集合体であるデータベースを Alice では **DS Manager(DSM)** と呼ぶ。 | |
46 * DSM 内の DS には対応する一意の String型のkey が存在し、 DSM 名と key を指定しすることで DS の保存、取得を行う。 | |
47 ![opt](./pictures/key.svg){:width="50%"} | |
48 | |
49 # Data Segment Manager | |
50 * Local DSM … 各ノード固有のデータベース | |
51 * Remote DSM … 他のノードのLocal DSMのproxy。接続しているノードの数だけ存在する。 | |
52 * Remote DSMに書き込むと対応するノードのLocalDSMに書き込まれる | |
176 | 53 * Remote DSMにはString型のDSM keyを指定してアクセスする |
175 | 54 ![opt](./pictures/newDSM.svg){:width="50%"} |
55 | |
176 | 56 # Data Segment API |
57 * DSの取得 | |
179 | 58 * take/peek |
176 | 59 * DSの追加 |
179 | 60 * put/update |
61 * DSの転送 | |
62 * flip | |
176 | 63 |
64 # Code Segmentの記述例 | |
65 * take/peekをするにはcreate/setKeyメソッドを使わなければならない | |
66 * *create* でインプットDGのRecieverを作り、*setKey* でReceiverにインプットとなるkeyを指定 | |
67 * データをReceiverから取り出す際は *asClass()* で型を指定 | |
68 * 処理をループさせたい場合はCSをnewする | |
69 ```java | |
70 public class TestCodeSegment extends CodeSegment { | |
71 private Receiver input = ids.create(CommandType.TAKE); | |
72 | |
73 public TestCodeSegment() { | |
74 input.setKey("count"); | |
75 } | |
76 | |
77 @Override | |
78 public void run() { | |
79 int count = input.asClass(Integer.class); | |
80 System.out.println("data = " + count); | |
81 | |
82 new TestCodeSegment(); | |
83 | |
84 ods.put("count", count); | |
85 } | |
86 } | |
87 ``` | |
88 | |
175 | 89 # Computation と Meta Computation |
90 * Aliceでは、計算の本質的な処理をComputatin、Computationとは別のレベルでそれを支える処理をMeta Computationとして分けて考える。 | |
91 * Alice のComputationは、keyによりDSを待ち合わせ、DSが揃ったCSを並列に実行する処理 | |
92 * Meta Computationはそれを実現している処理 | |
93 * DSの待ち合わせ | |
94 * 分散トポロジーの構成 | |
95 * 通信の切断・再接続時の処理 | |
96 * データの表現形式の選択 | |
97 | |
98 # Computation と Meta Computation | |
99 * 分散環境構築などの複雑な処理をAliceがMeta Computationとして提供する | |
100 * プログラマは目的の処理だけ記述し通信部分などはMeta Computationを指定する | |
101 * シンプルで見通しの良いコードを保つ | |
102 | |
176 | 103 # AliceのMeta Computation - Topology Manager/Topology Node |
175 | 104 * Topology Manager |
105 * ノード間の接続管理やトポロジーの構成管理行うMeta Computation | |
106 * Static Topology ManagerとDynamic Topology Managerがある | |
107 * Topology Node | |
108 * 各ノード側でTopology Managerとの通信を行うMeta Computation | |
109 * ノードアプリケーションを記述する際にTopology Nodeをnewしておけば以降のTopology Managerとの通信やノード間の接続を行う | |
110 | |
179 | 111 # AliceのMeta Computation - Topology Manager/Topology Node |
112 * Topology Managerを立ち上げる | |
175 | 113 * 各Topology NodeはTopology Managerに参加表明をし接続すべきノードの情報を要求する |
114 ![opt](./pictures/tree1.svg){:width="60%"} | |
115 | |
179 | 116 # AliceのMeta Computation - Topology Manager/Topology Node |
175 | 117 * 参加表明があった順に各ノードにnodeNameを割り当て、接続するべきノードのIPアドレス/ポート番号を送る |
118 ![opt](./pictures/tree2.svg){:width="60%"} | |
119 | |
179 | 120 # AliceのMeta Computation - Topology Manager/Topology Node |
175 | 121 * Topology Nodeが受け取った情報をもとにRemote DSMを立ちあげ接続し合うことでオーバーレイネットワークが作られる |
122 * Topology Managerは接続情報を管理し、実際の接続はTopology Nodeが行う | |
123 ![opt](./pictures/tree3.svg){:width="60%"} | |
124 | |
176 | 125 # AliceのMeta Computation - 圧縮 |
126 * DSは内部に圧縮・非圧縮の複数の形式を複数もつことができる | |
127 * 圧縮したデータの伸長と圧縮したままの転送が同時に可能 | |
128 ![opt](./pictures/compress.svg){:width="80%"} | |
175 | 129 |
176 | 130 # AliceのMeta Computation - 圧縮 |
131 * 圧縮の指定には宛先DGM keyに"compressed"とつけるだけでよい | |
132 `put("compressedRemoteDGM", "key", data)` | |
133 * 伸長も *asClass()* した際に自動でされる | |
134 * コードの変更が抑えて圧縮・非圧縮が切り替えられる | |
175 | 135 |
179 | 136 # AliceのNATを越え |
137 * NATを越えたノード間通信は分散処理の課題である | |
138 * Aliceではトポロジー管理がアプリケーションから分離しているため、コードを大きく変更しなくともTopology Managerを増やすことでNAT越えが可能 | |
175 | 139 |
179 | 140 # AliceのNATを越え接続 |
176 | 141 * 各プライベートネットワーク内を管理するPrivate Topology Manager |
142 * グローバルIPアドレスを持ったGlobal Topology Managerを1つ立てる | |
143 * TopologyNodeが複数対応できるためPrivate/Global Topology Managerに接続 | |
144 ![opt](./pictures/overNAT.svg){:width="70%"} | |
175 | 145 |
146 | |
147 # 複数のTopology Managerへの対応 | |
176 | 148 * 別トポロジーのアプリケーションの連携やNAT越えはノードが複数のTopologyManagerに接続することで可能になる |
175 | 149 * この機能を実現するにはTopology Nodeが各Topology Managerに対応する複数のnodeNameを持つようにする必要がある |
150 * Topology Nodeは割り当てられたnodeNameをDSとして保持してTopology Managerと通信を行うため、nodeNameの衝突を避けなければならない | |
151 | |
152 ![opt](./pictures/somehostname.svg){:width="50%"} | |
153 | |
154 # Local DSMの切り替えによる対応 | |
155 * 通常のLocal DSMとは別にTopology ManagerごとのLocal DSMを作成しnodeNameを管理 | |
156 * Tpology Manager/Nodeの働きはそのままに、指定するLocal DSMを変えるだけでTopology Managerの複数対応が可能 | |
157 ![opt](./pictures/somehostname2.svg){:width="50%"} | |
158 | |
176 | 159 |
160 # Aliceの問題点 - LocalDSMを複数立ち上げられない | |
161 * AliceではDSMを管理するクラスがstaticで書かれていたためLocal DSMを複数立ち上げることができない | |
162 * このstaticを抜くにはAliceのコード全体を大きく変更しなければならない | |
179 | 163 * 現状ではNAT越えのMeta Computationの追加が困難 |
176 | 164 * 複数インスタンスを立ち上げての分散プログラムのテストが書けない |
179 | 165 * 再設計の必要がある |
176 | 166 |
167 # Aliceの問題点 - APIシンタックスの分離 | |
179 | 168 * setKeyは記述場所が決まっておらず、待ち合わせを行っているCSの外からも呼べる |
169 * どのkeyを待っているのか不明なCSが生まれてしまう | |
176 | 170 * setKeyではkeyを動的に指定することができる |
171 * どんな処理を行っているかわかりづらい | |
172 * 対応するput箇所も修正しなければならない | |
173 * モデル検査しづらくなる | |
175 | 174 |
176 | 175 # Aliceの問題点 - APIシンタックスの分離 |
176 * setKeyは全てのcreateが終わった最後に呼ばなければならない | |
177 * Input DGの待ち合わせを行うカウンタはcreateの総数を持っている | |
178 * カウントが0になると入力が揃ったと判断しrunに入る | |
179 * countとsetKeyを交互に書くと入力が揃わないまま実行されNullPointExceptionになる | |
180 ```java | |
181 class ShowData extends CodeSegment{ | |
182 private Receiver[] info; | |
183 | |
184 public ShowData(int cnt) { | |
185 info = new Receiver[cnt]; | |
186 for (int i= 0;i < cnt; i++) { | |
187 info[i] = ids.create(CommandType.TAKE); | |
188 info[i].setKey(SetInfo.array[i]); | |
189 } | |
190 } | |
191 | |
192 @Override | |
193 public void run() { | |
194 int size = 0; | |
195 for (Receiver anInfo : info) { | |
196 DataList dlist = anInfo.asClass(DataList.class); | |
197 dlist.showData(); | |
198 } | |
199 } | |
200 } | |
201 ``` | |
175 | 202 |
176 | 203 # Aliceの問題点 - 型が推測できない |
204 * Input DSをReceiver型でcreateするため、どの型のデータを待っているのかわからない | |
205 * しかしReceiverからデータを取り出すにはasClass()で型を指定する必要がある | |
206 * 型をDSをputした箇所までコードをたどる必要がある | |
207 | |
208 # Aliceの問題点 - まとめ | |
209 * 以下の問題がAliceの信頼性・拡張性を下げている | |
210 * Local DSMを複数立ち上げられないため、Topology Managerの拡張やテストが困難 | |
211 * インプットAPIが分離しているためCSでどんな処理が行われているかわかりづらい | |
179 | 212 * setKeyの記述順序や型を気にしてプログラミングをしなくてはならない |
176 | 213 |
214 # 分散フレームワークChristieへの必要要件 | |
215 * Aliceの問題点を踏まえ、フレームワークをChristieを設計する | |
216 * staticなLocalDSMをなくし複数インスタンスを立ち上げられるようにすることでスケーラビリティを高める | |
217 * 煩雑なAPIをシンプルにし、記述性を高める | |
218 * 型の整合性をとれるようにし、信頼性を向上させる | |
219 | |
179 | 220 # Christie - 基本設計(1) |
176 | 221 * Javaで実装される |
179 | 222 * CS/DSの依存関係や、DSMの構造、リモートノードへの接続方法はAliceと同様である |
176 | 223 * 将来的に当研究室で開発しているGearsOSに統合したい |
224 * GearsOSに倣い、Code Gear(CG)/ Data Gear(DG) という名称を用いる | |
175 | 225 |
179 | 226 # Christie - 基本設計(2) |
227 * Code Gear Manager(CGM)という機構がData Gear Manager(DGM)を管理 | |
176 | 228 * 1つのCGMは1つのLocalDGMを持つ |
229 * CGM同士はThreadPoolとCGMのリストを共有している | |
230 * メタ計算で全てのCGMにアクセス可能 | |
231 ![opt](./pictures/ChristieClass.svg){:width="60%"} | |
232 | |
179 | 233 # Christie - 基本設計(2) DGMの複数立ち上げ |
176 | 234 * ChristieではCGMを2つ生成すればLocalDGMも2つ作られる |
179 | 235 * NAT越えなどの機能拡張に対応可能 |
176 | 236 * 複数のLocalDGM同士のやりとりは、Remoteへの接続と同じようにRemoteDGMを介してアクセスする |
179 | 237 * 分散プログラムのローカルでのテストが可能になる |
176 | 238 ![opt](./pictures/DGM.svg){:width="50%"} |
239 | |
179 | 240 # Christie - 基本設計(3) |
241 * CG を記述する際は Alice同様CodeGear.classを継承 | |
242 * CGは *void run(CodeGearManager cgm)* を持ち、run メソッド内に処理を記述 | |
243 * run内で新たなCGを作るためのAPIにはCGM経由で呼び出す | |
244 * このようにCGMを持ち運ぶ書き方はGearsOSに合わせてた書き方 | |
176 | 245 |
246 # Christie - アノテーションを用いたインプット記述 | |
247 * keyの指定にはJavaのアノテーションを用いる | |
248 * 先頭を@で始める注釈 | |
249 * 独自アノテーションを定義できる | |
250 * アノテーションから待ち合わせを行う処理にはJavaのreflectionAPIを使用 | |
175 | 251 |
176 | 252 # Christie - アノテーションを用いたインプット記述 |
253 * InputのためのDGを宣言し、その上にアノテーションでkeyを指定 | |
254 * Takeの例 | |
255 ```java | |
256 @Take(”count”) | |
257 public DataGear<Integer> count = new DataGear<>(); | |
258 ``` | |
259 * RemoteTakeの例 | |
260 ```java | |
261 @RemoteTake(dgmName="remote", key=”count”) | |
262 public DataGear<Integer> count = new DataGear<>(); | |
263 ``` | |
175 | 264 |
176 | 265 # Christie - アノテーションを用いたインプット記述 |
266 * アノテーションは必ずフィールドに付けなければならない | |
267 * InputDGの生成とkeyの指定を一箇所に書ける | |
268 * アノテーションの内容はコンパイル時に決定される | |
269 * 動的なkey指定を防ぐ | |
175 | 270 |
176 | 271 # Christie - 型を指定しないデータ取り出し |
272 * InputDGを宣言する際には必ず型の指定が必要となるため、CG内で型を把握できる | |
273 * DataGearはJavaの総称型を用いて<>内に指定した型を受け取る | |
274 ```java | |
275 @Take(”count”) | |
276 public DataGear<Integer> count = new DataGear<>(); | |
277 ``` | |
175 | 278 |
176 | 279 # Christie - 型を指定しないデータ取り出し |
280 * 宣言された型は内部で保存され、ノード間通信でも保たれる | |
281 * AliceのasClass()と違い、getData()で型を指定せずにデータを取り出すことができる | |
282 ```java | |
283 public class GetData extends CodeGear{ @Take(”name”) | |
284 public DataGear<String> name = new DataGear<>(); | |
285 | |
286 @Override | |
287 protected void run(CodeGearManager cgm) { | |
288 System.out.println(”this name is : ” + name.getData()); | |
289 } | |
290 } | |
291 ``` | |
292 * 取得したDGが待ち合わせに指定した型と違う場合はエラーになる | |
175 | 293 |
178
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
294 # Christie - まとめ |
179 | 295 * CodeGearManagerというDGMの管理機構を作ったことでLocalDGM複数立ち上げが可能になり、NAT越えなどの機能拡張やテストをしやすくなった |
176 | 296 * アノテーションを用いたことでDG生成とkey指定の分離問題を解決し、処理の見通しを良くした |
297 * 型の整合性を保証することで信頼性が向上した | |
175 | 298 |
176 | 299 # Christieと他フレームワークの比較 |
179 | 300 * Akka、Hazelcastと比較してChristieの特徴を述べる |
301 * Akka ...Scala/Java向け分散フレームワーク | |
302 * Hazelcast ...Java向け分散フレームワーク | |
175 | 303 |
176 | 304 # Christieと他フレームワークの比較 - Akka |
305 * アクターモデル | |
306 * アクターと呼ばれるオブジェクト同士が並列で非同期メッセージを送受信するモデル | |
307 * アクターは固有のアドレス持つ | |
308 * ローカルアクターにもリモートアクターにもアドレス指定でメッセージを送受信 | |
309 * アクターはメールボックスというキューを持つ | |
310 * 受け取ったメッセージをパターンマッチで順次処理 | |
178
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
311 * パターンマッチにはScalaのcase classを用いられる |
179 | 312 ![opt](./pictures/Akka.svg){:width="70%"} |
175 | 313 |
176 | 314 # Christieと他フレームワークの比較 - Hazelcast |
315 * キーと値の1対1でデータを管理するインメモリ・データグリッド | |
316 * 複数のノードに分散させたデータを、仮想的な1つのメモリ空間に見せるモデル | |
317 * プログラマがサーバを意識せずに共有のタプルスペースに対してデータをget/put | |
178
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
318 * 共有のタプルスペースに書き込むとマルチキャストで全サーバにデータが送られる |
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
319 ![opt](./pictures/Hazelcast.svg){:width="50%"} |
175 | 320 |
176 | 321 # Christieと他フレームワークの比較 - 設計思想 |
322 * AkkaやHazelcastはロケーション透過性が高く、分散プログラムの煩雑な処理を抽象度を高めることで隠している | |
178
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
323 * Christieでは分散性を明示的に意識しながら記述できるためチューニングしやすい |
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
324 * 通常計算とメタ計算に分けているため複雑さを下げている |
175 | 325 |
176 | 326 # Christieと他フレームワークの比較 - 記述性 |
178
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
327 * アノテーションを使ったインプットの指定はAkkaやHazelcastにはない |
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
328 * 複数のインプットを待ち合わせして処理を行いたい場合 |
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
329 * Akkaは待ち合わせ処理をプログラマが書かなければならない |
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
330 * Christieでは複数のインプットを記述でき待ち合わせ処理が必要ない |
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
331 * データの圧縮通信を指定したい場合 |
074eb76a9184
add Akka/Hazelcast slide
Nozomi Teruya <e125769@ie.u-ryukyu.ac.jp>
parents:
177
diff
changeset
|
332 * Akka、Hazelcastでは圧縮メソッドが用意されているため、それを用いて記述する |
179 | 333 * ChristieではDGMkeyの名前を変えるだけでメソッド呼び出しの記述が要らないため少ない変更で拡張が可能 |
175 | 334 |
335 # まとめ | |
176 | 336 * AliceのプロトコルやMeta Computationを説明し、TopologyManagerを用いたNAT越えの手法を示した |
337 * Aliceの問題点を整理し、再設計の必要性を述べた | |
338 * LocalDGMの複数立ち上げを可能にし、テストや機能拡張がしやすい環境を整えた | |
339 * Christieではアノテーションを用いたAPIで信頼性の高い記述を実現した | |
179 | 340 * Christieを他のフレームワークと比較し、分散性を意識して記述できる特徴があることを示した |
175 | 341 |
176 | 342 # 今後の課題 |
343 * DataGearのメタレイヤーへの移行 | |
344 * TopologyManagerの実装 | |
345 * 実用性の検証 | |
346 * Jungleとの統合 | |
347 * GearsOSへの移行 | |
175 | 348 |
179 | 349 # Christie - CGの生成方法 |
350 1. StartCodeGear.classを継承しCGMを生成する | |
351 2. CGをnewしたあと*setup*を用いる | |
352 * newが終わらないとアノテーションから待ち合わせを行う処理ができないため | |
353 * このときCGMがCGに渡されるため、プログラマが引数にCGMを渡す必要はない | |
354 | |
355 ```java | |
356 public class StartTest extends StartCodeGear{//StartCG | |
357 | |
358 public StartTest(CodeGearManager cgm) { | |
359 super(cgm); | |
360 } | |
361 | |
362 public static void main(String args[]){ | |
363 StartTest start = new StartTest(createCGM(10000));//CGMを生成 | |
364 } | |
365 | |
366 @Override | |
367 protected void run(CodeGearManager cgm) { | |
368 cgm.setup(new TestCodeGear());//CGの待ち合わせを開始 | |
369 getLocalDGM().put("count", 1); | |
370 } | |
371 } | |
372 ``` | |
373 | |
374 | |
175 | 375 <style type="text/css"> |
376 <!-- | |
377 *{ | |
378 font:nomal 100% 'PT Sans'; | |
379 } | |
380 | |
381 ul > li{ | |
382 list-style-type:disc; | |
383 } | |
384 | |
385 .slide h1{ | |
386 text-align:left; | |
387 color:#777777; | |
388 font:bold 40px/1.13 'PT Sans', sans-serif; | |
389 margin-bottom: 50px; | |
390 } | |
391 | |
392 div#slide1 h1{ | |
393 text-align:left; | |
394 color:#777777; | |
395 font:bold 60px 'PT Sans', sans-serif; | |
396 margin-bottom: 50px; | |
397 } | |
398 | |
399 pre > code{ | |
400 font-family:'Droid Sans Mono', 'Courier New', monospace; | |
401 } | |
402 | |
403 img[alt="opt"]{ | |
404 display: block; | |
405 margin-left: auto; | |
406 margin-right: auto; | |
407 } | |
408 | |
409 img[alt="right"]{ | |
410 margin-right: 0; | |
411 } | |
412 | |
413 table { | |
414 margin-left: auto; | |
415 margin-right: auto; | |
416 } | |
417 | |
418 th { | |
419 font-size: 120%; | |
420 } | |
421 --> | |
422 </style> |